Fail-Fast vs Fail-Safe Iterators
How modCount makes an iterator fail fast, why removing inside a for-each throws, the four correct ways to remove while iterating, and what weakly consistent iteration actually promises.
On this page
ConcurrentModificationException is one of the first exceptions most Java developers meet and one of
the most misunderstood. It is not about concurrency, it is not a guarantee, and the version of the
bug that does not throw is considerably more dangerous.
Key Takeaways
- Fail-fast iterators compare a snapshot of
modCountagainst the collection's current value on everynext(). ConcurrentModificationExceptionis best-effort detection of a programming error, not a thread-safety feature. Single-threaded code triggers it constantly.- Removing the second-to-last element does not throw — it silently skips one.
- Correct removal:
Iterator.remove(),removeIf, iterate a copy, or collect the survivors. CopyOnWriteArrayListgives a snapshot;ConcurrentHashMapgives weak consistency. They are different guarantees.
The mechanism
Every ArrayList, HashMap and their relatives keep an int modCount, incremented on any
structural modification — one that changes the size, or (for a map) rehashes the table. Setting an
existing element's value is not structural and does not increment it.
private class Itr implements Iterator<E> {
int cursor;
int expectedModCount = modCount; // snapshot taken at iterator creation
public boolean hasNext() {
return cursor != size; // note: no modCount check here
}
public E next() {
checkForComodification(); // the check lives here
...
}
final void checkForComodification() {
if (modCount != expectedModCount) throw new ConcurrentModificationException();
}
}A for-each loop is compiled into exactly this iterator, which is why the exception appears in code that contains no visible iterator at all:
List<String> names = new ArrayList<>(List.of("ada", "grace", "alan", "edsger"));
for (String name : names) {
if (name.startsWith("a")) {
names.remove(name); // modCount++ — the iterator's snapshot is now stale
}
}
// ConcurrentModificationException on the next call to next()The version that does not throw
List<String> list = new ArrayList<>(List.of("a", "b"));
for (String s : list) {
if (s.equals("a")) list.remove(s);
}
// No exception. list is now ["b"], and "b" was never visited by the loop.Walk it through: after removing "a" the cursor is 1 and the size is 1, so hasNext() returns
false and next() — the only method that validates modCount — is never reached. This is why
"just wrap it in a try/catch" is the wrong response to ConcurrentModificationException: the
exception is the good outcome.
Four correct ways
Iterator<String> it = names.iterator();
while (it.hasNext()) {
if (it.next().startsWith("a")) {
it.remove(); // updates expectedModCount as well as the list
}
}names.removeIf(name -> name.startsWith("a"));removeIf is not just shorter. On an ArrayList it compacts in a single pass, so removing k
elements from a list of n is O(n) rather than the O(n·k) of repeated remove(i) calls. For large
lists this is the difference between milliseconds and minutes.
for (String name : new ArrayList<>(names)) {
if (shouldRemove(name)) {
names.remove(name);
auditLog.record(name); // side effects the removeIf predicate should not have
}
}List<String> kept = names.stream()
.filter(name -> !name.startsWith("a"))
.toList();Option 4 is usually the best design: it produces an immutable result, has no ordering hazards, and makes the intent obvious. Option 3 exists for the case where the loop body has side effects that do not belong in a predicate.
For maps, the same shapes apply through the views:
map.entrySet().removeIf(e -> e.getValue().isExpired());
map.values().removeIf(Order::isCancelled);
map.keySet().removeIf(key -> key.startsWith("tmp-"));
// Or with an explicit iterator
Iterator<Map.Entry<String, Order>> it = map.entrySet().iterator();
while (it.hasNext()) {
if (it.next().getValue().isExpired()) it.remove();
}Fail-safe, and what it actually means
"Fail-safe" is informal terminology — the JDK documents two distinct behaviours, and conflating them is a common interview mistake.
Snapshot iteration — CopyOnWriteArrayList, CopyOnWriteArraySet. The iterator holds a
reference to the array as it was when iteration began. Later modifications create a new array and
are completely invisible to the running iterator. The iterator cannot support remove(), because
there is nothing meaningful to remove from.
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(List.of("a", "b"));
for (String s : list) {
list.add("c"); // no exception; the loop still runs exactly twice
}
list.size(); // 4 — two "c" were addedEvery mutation copies the entire backing array, which is O(n) per write. That is only acceptable when reads vastly outnumber writes — a listener registry, a rarely-changed configuration list.
Weakly consistent iteration — ConcurrentHashMap, ConcurrentLinkedQueue,
ConcurrentSkipListMap. The iterator does not take a snapshot. It guarantees: no
ConcurrentModificationException; every element present for the whole traversal is returned exactly
once; and elements added or removed during traversal may or may not be observed.
| Fail-fast | Snapshot | Weakly consistent | |
|---|---|---|---|
| Examples | ArrayList, HashMap | CopyOnWriteArrayList | ConcurrentHashMap |
| Throws CME | Yes (best effort) | No | No |
| Sees later changes | — | Never | Maybe |
Iterator.remove() | Supported | Unsupported | Supported |
| Memory cost | None | Full copy per write | None |
Not a thread-safety mechanism
The javadoc is explicit: "this exception may be thrown by methods that have detected concurrent modification... Note that fail-fast behaviour cannot be guaranteed... programs that depend on this exception for their correctness would be erroneous."
Two consequences. First, modCount is a plain non-volatile int, so under genuine multi-threaded
access one thread may never observe another's increment and the exception simply does not fire —
while the collection corrupts anyway. Second, the exception can fire spuriously in code that is
otherwise correct.
So: use it as what it is, a debugging aid that catches an accidental mutation during iteration in single-threaded code. For actual concurrent access, use a concurrent collection.
What gets asked
The standard sequence is: what is ConcurrentModificationException; how does the collection detect
it; how do you remove elements correctly; and what is the difference between fail-fast and fail-safe.
The answer that stands out is volunteering the second-to-last-element case. Most candidates can
recite Iterator.remove(); far fewer know that the same bug can silently skip an element instead of
throwing, and explaining why — hasNext() compares the cursor to the size, not the modCount —
demonstrates you have actually read the implementation.
Frequently Asked Questions
Why does removing the second-to-last element in a for-each loop not throw?
Is ConcurrentModificationException always caused by multiple threads?
What does weakly consistent mean?
Related tutorials
- Queues, Deques and BlockingQueuesThe three method families and why Queue has three ways to insert, choosing between ArrayBlockingQueue and LinkedBlockingQueue, PriorityQueue as a binary heap, and the SynchronousQueue handoff.
- Choosing a Collection: Complexity and Memory FootprintA complete Big-O table for every common collection, what each one actually costs per element in bytes, why boxing dominates numeric collections, and a decision procedure that fits on one page.
- HashSet, LinkedHashSet and TreeSetWhy every Set is a Map underneath, how iteration order differs, the TreeSet comparator-equality trap, EnumSet as a bit vector, and choosing a Set for concurrent access.
- TreeMap, LinkedHashMap and Building an LRU CacheHow TreeMap uses a red-black tree for sorted keys and range queries, how LinkedHashMap adds a doubly-linked list for ordering, and building an LRU cache in ten lines with removeEldestEntry.