The fail-fast contract is 'best-effort', not guaranteed: a case that does NOT throw =================================================================================== list before: [10, 20] (size=2) elements the loop actually saw before ending: [10] list after the loop: [20] ConcurrentModificationException thrown: false Why: after removing 10, size becomes 1 and the iterator's cursor is already 1 (it advanced to 1 when next() returned 10). ArrayList.Itr.hasNext() is just 'return cursor != size;' - no modCount check - so hasNext() sees 1 != 1, returns false, and the loop ends normally. next() is the only method that checks modCount, and it is never called again. The element 20 is silently never visited.