ruby-concurrency/concurrent-ruby · error · IllegalStateException
IllegalStateException
Error message
IllegalStateException
What it means
Every iterator and enumeration from keySet(), values(), entrySet(), keys() and elements() is backed by the internal Traverser class. Its remove() needs a key latched by a previous advance/next; if nextKey is null and advance() can find no element, there is nothing to remove and it throws IllegalStateException. Note this implementation deviates from java.util iterators: remove() before the first next() on a NON-empty map silently deletes an element instead of throwing, and a double remove can quietly no-op rather than throw.
Source
Thrown at ext/concurrent-ruby/com/concurrent_ruby/ext/jsr166e/ConcurrentHashMapV8.java:2501
if ((ek = e.key) instanceof TreeBin)
e = ((TreeBin)ek).first;
else {
tab = (Node[])ek;
continue; // restarts due to null val
}
} // visit upper slots if present
index = (i += baseSize) < n ? i : (baseIndex = b + 1);
}
nextKey = (K) e.key;
} while ((ev = (V) e.val) == null); // skip deleted or special nodes
next = e;
return nextVal = ev;
}
public final void remove() {
Object k = nextKey;
if (k == null && (advance() == null || (k = nextKey) == null))
throw new IllegalStateException();
map.internalReplace(k, null, null);
}
public final boolean hasNext() {
return nextVal != null || advance() != null;
}
public final boolean hasMoreElements() { return hasNext(); }
public final void setRawResult(Object x) { }
public R getRawResult() { return null; }
public boolean exec() { return true; }
}
/* ---------------- Public operations -------------- */
/**
* Creates a new, empty map with the default initial table size (16).
*/View on GitHub (pinned to 0b88d5ff75)
Solutions
- Call it.remove() only immediately after a successful it.next() returned the element you want deleted
- Track removal eligibility with a boolean set by next() and cleared after remove()
- Prefer collecting keys during iteration and calling map.remove(key) after the loop instead of iterator remove()
- Restructure the loop so next() always executes before any remove() can run
Example fix
// before
Iterator<String> it = map.keySet().iterator();
if (shouldDropFirst) it.remove(); // IllegalStateException on empty map
// after
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
String k = it.next();
if (shouldDrop(k)) it.remove();
} Defensive patterns
Strategy: validation
Validate before calling
Iterator<K> it = map.keySet().iterator();
while (it.hasNext()) {
K k = it.next(); // always latch an element first
if (shouldDrop(k)) it.remove();
} Try / catch
try { it.remove(); } catch (IllegalStateException e) { // no element latched: iterator exhausted or map empty - fix the loop structure, do not ignore } Prevention
- Always pair remove() with a preceding next() in the same iteration
- Prefer map.remove(key) loops over iterator.remove under concurrency
- Do not assume a second remove throws here - it can silently no-op
When it happens
Trigger: Calling it.remove() before the first next() when the map is empty; calling it.remove() after traversal finished and concurrent removals emptied the map; a filtering loop where the it.next() call is conditional but it.remove() is unconditional, so remove() eventually runs with no latched element.
Common situations: Hand-rolled filter loops that break the next-before-remove pairing required by all JDK iterators; racing threads draining the map while another thread iterates; porting code between for-each and while-hasNext styles and losing the next() call.
Related errors
- NullPointerException
- can't dump hash with default proc
- key not found
- :initial_capacity must be a positive Integer
- :load_factor must be a number between 0 and 1
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/f07a371c46dbed8a.
Report an issue: GitHub.