java.util.ConcurrentModificationException

Quick answer

You added to or removed from a collection while looping over it. Despite the name it's almost always single-threaded. Fix it by modifying through the iteration:

  • Removing by a condition? Use list.removeIf(x -> ...) — the cleanest fix.
  • Need the Iterator directly? Call iterator.remove(), not list.remove().
  • Must modify the original while reading it? Iterate a copy.
  • Actually shared across threads? Use ConcurrentHashMap / CopyOnWriteArrayList.

The exact stack trace

List<String> items = new ArrayList<>(List.of("a", "b", "c"));
for (String item : items) {
    if (item.equals("b")) {
        items.remove(item);   // modifying while iterating
    }
}
// Exception in thread "main" java.util.ConcurrentModificationException
//     at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
//     at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)

The name is the biggest source of confusion: “concurrent” does not mean threads here. It means the collection was modified concurrently with an in-progress iteration — and the example above, a single-threaded for-each loop, is the textbook trigger. Most collections use fail-fast iterators that detect this and throw immediately, rather than silently returning wrong results.

The reason a plain for-each loop can trip this at all is that it secretly uses an Iterator. The compiler desugars it — for (String s : list) becomes, roughly:

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();      // ← the check runs here, on every next()
    // ... your loop body ...
}

So calling list.remove(s) in the loop body modifies the list behind that hidden iterator's back, and the very next it.next() catches it. Seeing the desugared form makes the fix obvious: change the collection through it, not through list.

Why it throws: the fail-fast modification count

for-each starts iterating iterator.expectedModCount = 3 list.remove("b") list.modCount 3 → 4 next() call: compare counts expected 3 vs actual 4 3 ≠ 4 → fail-fast ConcurrentModificationException Fix: modify THROUGH the iterator iterator.remove() / removeIf() keep the counts in sync

Every structural change bumps the list's modCount. The iterator remembers the count it started with; when they diverge, next() throws. Removing through the iterator updates both counts together.

Why does set() work but remove() throws?

A common surprise: replacing an existing element while iterating is perfectly fine, while adding or removing one throws. The rule is the word structural — only changes to the collection's size bump modCount:

List<String> items = new ArrayList<>(List.of("a", "b", "c"));
for (int i = 0; i < items.size(); i++) {
    items.set(i, items.get(i).toUpperCase());   // ✅ not structural — modCount unchanged
}

List.set(index, value) overwrites a slot without changing the size, so it doesn't touch modCount and never trips a fail-fast iterator. Only add and remove (and their bulk cousins) are structural modifications. That's why the fixes below are all about how you add or remove during iteration — replacing values in place was never the problem.

Fix 1: removeIf (the cleanest, Java 8+)

For conditional removal, removeIf is a one-liner that handles the iteration internally and never trips the check:

items.removeIf(item -> item.equals("b"));   // ✅ no loop, no exception

Fix 2: Iterator.remove()

When you need explicit control, obtain the Iterator yourself and remove through it — this updates the iterator's expected count in lockstep with the list's:

Iterator<String> it = items.iterator();
while (it.hasNext()) {
    String item = it.next();
    if (item.equals("b")) {
        it.remove();   // ✅ removes via the iterator — counts stay in sync
    }
}

The distinction is the whole point: it.remove() works, items.remove(item) inside the same loop throws.

One rule to know so you don't swap this error for another: Iterator.remove() may be called only once per next(), and only after a successful next(). Calling it twice in a row, or before the first next(), throws IllegalStateException instead — a different exception people often hit while trying to fix this one.

Fix 3: iterate over a copy

When you must both read every original element and mutate the source (for example adding new elements, which removeIf can't do), iterate a snapshot so the two collections are independent:

for (String item : new ArrayList<>(items)) {   // iterate a copy
    if (shouldReplace(item)) {
        items.remove(item);
        items.add(transform(item));           // ✅ modifying the original is safe now
    }
}

It costs one array copy, but it's simple and correct when the modification isn't a pure conditional delete.

Why the examples wrap List.of() in a new ArrayList

You'll notice the examples here start with new ArrayList<>(List.of("a", "b", "c")) rather than List.of(...) directly. That's deliberate, and it heads off a different error you can hit while fixing this one: List.of() (and Arrays.asList(), Collections.unmodifiableList(), and streams' toList()) returns an immutable list. Calling remove(), add(), or removeIf() on it doesn't throw ConcurrentModificationException — it throws UnsupportedOperationException, because the list forbids any structural change, iteration or not:

List<String> fixed = List.of("a", "b", "c");
fixed.removeIf(s -> s.equals("b"));   // ❌ UnsupportedOperationException, not CME

List<String> mutable = new ArrayList<>(fixed);   // ✅ a real, modifiable copy
mutable.removeIf(s -> s.equals("b"));            // ✅ works

So if removeIf or Iterator.remove() gives you UnsupportedOperationException instead of the error on this page, the collection is immutable — wrap it in a new ArrayList<>(...) first.

Fix 4: when it really is threads

If two threads genuinely share the collection — one iterating, one modifying — fail-fast iterators may throw, but that's an unreliable signal, not a fix. Use a collection built for concurrent access instead:

// reads and writes from many threads
Map<String, Integer> counts = new ConcurrentHashMap<>();

// safe iteration under concurrent writes (snapshot semantics)
List<String> listeners = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList gives iterators a stable snapshot (great for read-heavy, write-rare cases like listener lists); ConcurrentHashMap supports concurrent reads and writes without fail-fast iteration. Don't reach for these for the single-threaded case, though — there, Fixes 1–3 are correct and cheaper.

Watch out for a false fix here: wrapping a list in Collections.synchronizedList(list) does not make iteration safe. It synchronizes each individual method call, but a whole iteration is many calls, so another thread can still modify the list mid-loop and trigger the exception. You have to synchronize the entire iteration yourself:

List<String> list = Collections.synchronizedList(new ArrayList<>());
// ...
synchronized (list) {                 // ✅ required — synchronizedList alone isn't enough
    for (String item : list) { /* ... */ }
}

This is a classic “I did what the docs said and it still breaks” trap — synchronizedList protects single operations, not compound ones like iterating.

Adding during iteration: use a ListIterator

Iterator can only remove. If you need to add or replace elements while walking a list, use a ListIterator, which exposes add() and set() that keep the modification counts in sync just like remove() does:

ListIterator<String> it = items.listIterator();
while (it.hasNext()) {
    String item = it.next();
    if (item.startsWith("temp-")) {
        it.set(item.substring(5));   // ✅ replace in place, no exception
    }
}

Watch out for Map iteration

The same rule applies to maps, just through a different door. Iterating map.keySet() or map.entrySet() and calling map.remove(key) inside the loop throws the exact same exception. Remove through the entry-set iterator, or — cleanest on Java 8+ — use values().removeIf(...) or entrySet().removeIf(...):

// ❌ throws — modifying the map while iterating its entrySet
for (var entry : scores.entrySet()) {
    if (entry.getValue() == 0) scores.remove(entry.getKey());
}

// ✅ removeIf on the entry set
scores.entrySet().removeIf(e -> e.getValue() == 0);

Or build a new collection with a Stream

When you don't actually need to mutate the original at all, the most declarative fix is to filter into a new collection and leave the source untouched:

List<String> filtered = items.stream()
                             .filter(item -> !item.equals("b"))
                             .toList();   // Java 16+; use collect(Collectors.toList()) before that

Streams create a new collection instead of modifying the one being iterated, so there is nothing for a fail-fast iterator to catch. This is often the cleanest choice when the “modification” you wanted was really just “a filtered version of this list” — you get an immutable result and the original stays intact.

One caveat: fail-fast is best-effort, not a guarantee

It's worth understanding the guarantee you're not getting. The Java documentation is explicit that fail-fast behavior cannot be guaranteed: the modification-count check is done on a best-effort basis, so a program must never depend on this exception for correctness — it exists only to detect bugs. In practice that means two things. First, in single-threaded code the exception is reliable enough to treat as a hard signal that you modified during iteration, and you should fix the code as shown above. Second, in multi-threaded code the absence of the exception proves nothing — a data race can corrupt a shared collection silently without ever tripping the check. So don't try to “catch and ignore” ConcurrentModificationException to paper over a threading problem; the correct response is always to fix the structure of the code, either by modifying through the iterator (single-threaded) or by using a concurrent collection (multi-threaded).

Debugging checklist

Frequently Asked Questions

What causes ConcurrentModificationException in Java?

You structurally modified a collection — added or removed an element — while iterating over it. Most collections' iterators are ‘fail-fast’: they track a modification count, and if the collection changes through anything other than the iterator's own methods during iteration, the next call to next() throws ConcurrentModificationException. It's a deliberate guard against iterating a collection whose structure shifted underneath you.

Why does it happen in single-threaded code with no concurrency?

The name is misleading. ‘Concurrent’ here means the collection was modified concurrently with iteration, not necessarily from another thread. The classic trigger is a single for-each loop that calls list.remove(x) on the same list it's iterating — one thread, no concurrency, still thrown. The fail-fast check fires the moment the modification count no longer matches what the iterator expected.

How do I remove elements from a list while iterating?

Use the iterator's own remove() method, or the collection's removeIf(). Either iterator.remove() (with an explicit Iterator obtained from the collection) or list.removeIf(x -> condition) modifies the collection through a path the iteration knows about, so the fail-fast check isn't tripped. removeIf is the cleanest one-liner for conditional removal on Java 8 and later.

Why does the enhanced for-each loop cause this?

A for-each loop compiles down to an Iterator under the hood. When you call collection.remove(x) inside the loop body, you modify the collection directly — bypassing the iterator — so on the next iteration the iterator detects the modification-count mismatch and throws. Because the removal is hidden behind familiar syntax, this is the most common way developers hit the exception without realizing they're modifying during iteration.

When is ConcurrentModificationException actually about threads?

When one thread iterates a collection while another thread modifies it. Fail-fast iterators may — but are not guaranteed to — throw in that case, so you can't rely on it for correctness. For genuinely shared collections use a thread-safe type such as ConcurrentHashMap or CopyOnWriteArrayList, or synchronize access externally, rather than depending on the exception to signal the race.

Does iterating over a copy fix it?

Yes. If you iterate over a snapshot — for example for (var x : new ArrayList<>(list)) — and modify the original list inside the loop, the iteration and the modification touch different objects, so no fail-fast check trips. It costs a copy but is simple and safe when you need to both read every original element and mutate the source. For pure conditional removal, removeIf is lighter.

More Java & backend errors

Browse the full reference for Java, Go, and Rust errors — exact message, cause, and fix.

All Error References Java NullPointerException Java ArrayIndexOutOfBounds
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.