Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Intermediate6 min readUpdated
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 modCount against the collection's current value on every next().
  • ConcurrentModificationException is 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.
  • CopyOnWriteArrayList gives a snapshot; ConcurrentHashMap gives 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.

how the check works, from ArrayList
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:

the classic
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

two elements, one removed, one never seen
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

1 — Iterator.remove(), the general answer
Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("a")) {
        it.remove();          // updates expectedModCount as well as the list
    }
}
2 — removeIf, the idiomatic one
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.

3 — iterate a copy when the loop must do more than remove
for (String name : new ArrayList<>(names)) {
    if (shouldRemove(name)) {
        names.remove(name);
        auditLog.record(name);   // side effects the removeIf predicate should not have
    }
}
4 — build the result instead of mutating
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:

maps
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 iterationCopyOnWriteArrayList, 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.

the snapshot
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 added

Every 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 iterationConcurrentHashMap, 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-fastSnapshotWeakly consistent
ExamplesArrayList, HashMapCopyOnWriteArrayListConcurrentHashMap
Throws CMEYes (best effort)NoNo
Sees later changesNeverMaybe
Iterator.remove()SupportedUnsupportedSupported
Memory costNoneFull copy per writeNone

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 whyhasNext() 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?
Because ArrayList hasNext() is implemented as cursor != size, not as a modCount check. After removing the second-to-last element the size drops to equal the cursor, so hasNext() returns false, the loop exits normally and next() — the method that actually validates modCount — is never called. The bug silently skips an element instead of throwing, which is worse than the exception.
Is ConcurrentModificationException always caused by multiple threads?
No, and the name is misleading. It most often fires in single-threaded code that modifies a collection while a for-each loop over it is in progress. It is a best-effort programming-error detector, not a thread-safety mechanism, and the javadoc says explicitly that you must not depend on it for correctness — it can also fail to fire when it should.
What does weakly consistent mean?
A weakly consistent iterator will traverse elements as they existed at some point at or after its creation, will never throw ConcurrentModificationException, will return each element at most once, and may or may not reflect modifications made after it started. ConcurrentHashMap and the concurrent queues work this way. It is not a snapshot — that is what CopyOnWriteArrayList gives you.

Related tutorials