01
Do not infer another thread from the exception name
Read the stack and inspect structural changes inside the loop or its callbacks. A helper can remove or add an element while a caller is still iterating over the same list, even in one thread.
The reproduction used only main. It attempted to remove stale values directly from the ArrayList while the enhanced for loop was using an iterator. The selected data made the failing traversal observable on the tested runtime.
02
Keep a small failing case
Save this intentionally incorrect example as UnsafeRemoval.java. Its list has two stale entries around one live entry. Run it only as a reproduction rather than including the failure as normal application control flow.
Fail-fast detection is best effort. Do not depend on every bad mutation pattern throwing this exception or treat the absence of an exception as evidence of thread safety.
import java.util.ArrayList;
import java.util.List;
public class UnsafeRemoval {
public static void main(String[] args) {
var states = new ArrayList<>(List.of("stale", "live", "stale"));
for (String state : states) {
if (state.equals("stale")) states.remove(state);
}
System.out.println(states);
}
}03
Use a supported mutation operation
For this mutable ArrayList, removeIf applies the filter through the collection’s operation. Save the following repair as SafeRemoval.java. Its assertion checks both that stale elements are removed and the live element remains.
The predicate does not structurally mutate the list itself. If your application must retain an immutable original, build an appropriate new collection instead; removeIf is a mutating operation and some collections do not support removal.
import java.util.ArrayList;
import java.util.List;
public class SafeRemoval {
public static void main(String[] args) {
var states = new ArrayList<>(List.of("stale", "live", "stale"));
states.removeIf(state -> state.equals("stale"));
if (!states.equals(List.of("live"))) throw new AssertionError(states);
System.out.println(states);
}
}04
Verify the complete result
Compile both examples. The failing process should show ConcurrentModificationException; the corrected one should print [live]. Verifying only that the exception disappeared could miss an implementation that silently skipped an element.
Use representative edge cases in the application: no matches, all matches, duplicate values, and an empty list. Keep mutation semantics explicit, especially when callbacks can reach the same collection.
javac -d out UnsafeRemoval.java SafeRemoval.java
# Expected diagnostic failure:
java -cp out UnsafeRemoval
# Expected result: [live]
java -cp out SafeRemoval05
Investigate real concurrent access independently
If multiple threads share the collection, removing this loop defect does not establish a safe concurrency design. Review ownership, synchronization, or the chosen concurrent collection’s documented iteration behavior.
Preserve the original mutation site and the expected output. Catching ConcurrentModificationException and retrying the same unsafe loop is not a reliable repair, because the exception is a bug signal rather than a coordination mechanism.
Know the limits
Reproduced with Eclipse Temurin OpenJDK 25.0.4.1+1 in a disposable Linux container with no external network. Removing directly from a three-element ArrayList during enhanced-for iteration raised ConcurrentModificationException. removeIf produced the expected single remaining element. This is a single-threaded ArrayList test, not validation of concurrent access or every Collection implementation.
Sources & further reading
A technical publication by CoreTrailKit.
Browse all articles