What are fail-fast and fail-safe iterators in Java, and what happens when you modify a collection while iterating it?
A fail-fast iterator checks the collection for concurrent modification as it goes. When you obtain an iterator, it records the collection's modification count. Every call to next compares the current count with the recorded one, and if they differ it throws ConcurrentModificationException rather than letting you continue iterating over a structure that has shifted underneath you.
The exception is thrown on the next call, not by the modification itself. So the modification succeeds, and then the iterator notices on the following step and gives up. The collection is left in a partially modified state, which is one reason this pattern is so awkward to debug.
A fail-safe iterator does not fail at all. It either works on a snapshot of the collection taken when the iterator was created, or it tolerates concurrent modification without throwing. The classic snapshot approach is the CopyOnWriteArrayList, whose iterator reads from an immutable copy, so it never sees later changes and never throws.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
try {
for (String s : list) {
if (s.equals("a")) {
list.add("d"); // structural modification during iteration
}
}
} catch (java.util.ConcurrentModificationException e) {
System.out.println("fail-fast: ConcurrentModificationException");
}
// A copy-on-write list iterates over a snapshot.
CopyOnWriteArrayList<String> safe = new CopyOnWriteArrayList<>(List.of("a", "b", "c"));
for (String s : safe) {
if (s.equals("a")) {
safe.add("d");
}
}
System.out.println("fail-safe: " + safe);
}
}The output shows both behaviours. The ArrayList iteration throws on the second pass through the loop. The CopyOnWriteArrayList completes normally and the new element is present in the list afterwards, even though the iteration never saw it — the iterator was working from a snapshot taken before the addition.
Two clarifications. Fail-fast is not a safety guarantee; it is a bug detector, and it is best-effort. The check catches modifications made through the same collection object, but it will not detect a modification made through a different reference to the same underlying data, and it will not fire if the modification does not change the modification count. And fail-safe does not mean thread-safe: the CopyOnWriteArrayList itself is thread safe, but a plain ArrayList with an iterator is still not.