🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
java.util.NoSuchElementException
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1053)
at com.devinhyderabad.batch.FeedMerger.drain(FeedMerger.java:64)⚡ Quick Fix Works 80% of the time
Guard every next() call with hasNext(), or drop the manual iterator and use enhanced-for.
while (iterator.hasNext()) {
String value = iterator.next();
process(value);
}🧠 Why this Happens
Tap to expand the deep technical explanation
An Iterator is not a copy of the data, it is a cursor holding one integer position. ArrayList$Itr.next() compares that position against size and throws NoSuchElementException when they are equal because there is no element slot left to return. The JVM never rescans the list: cursor position IS the state. Queue types behave identically - ArrayDeque.remove()/element() throw the same exception when head equals tail, which means zero elements. hasNext() is a pure peek that moves nothing, which is exactly why the guard works.
The HITEC City Parking Spot Analogy:
Flipping one more page after the last page of a notebook: your bookmark moved to the end, and another flip hands you nothing.
🔁 How to Reproduce Confirm this is your error
List<Integer> nums = new ArrayList<>(List.of(7)); Iterator<Integer> it = nums.iterator(); it.next(); // consumes the only element it.next(); // throws NoSuchElementException
🛠️ Solutions (5 Ways to Fix)
Guard next() with hasNext()
👉 Use this if you keep a manual iterator because you remove while reading or juggle two cursors over one list.
hasNext() asks whether a next slot exists without moving the cursor. Only advance when it answers true, so the state machine can never overshoot.
for (Iterator<String> it = items.iterator(); it.hasNext(); ) {
String item = it.next();
if (isJunk(item)) {
it.remove();
}
}Replace manual iteration with enhanced-for
👉 Use this if you do not mutate the list during the walk and just need every element.
The compiler expands for-each into correct hasNext()/next() pairs, so overshooting becomes impossible by construction.
for (String item : items) {
process(item);
}Use poll()/peek() on queues instead of remove()/element()
👉 Use this if a Queue or Deque can legitimately be empty at read time.
pollFirst()/peekFirst() are the null-returning twins of removeFirst()/getFirst(): an empty queue yields null instead of an exception, turning a crash path into a branch.
ArrayDeque<Task> queue = new ArrayDeque<>(tasks);
Task next;
while ((next = queue.pollFirst()) != null) {
run(next);
}Get a fresh iterator instead of reusing an exhausted one
👉 Use this if a second pass over the same collection needs its own cursor.
An exhausted iterator never rewinds. Calling collection.iterator() again returns a new cursor positioned before the first element.
firstPass.forEach(first::add); // pass 1 done
for (String s : firstPass) second.add(s); // fresh cursor, not the old oneCheck isEmpty() before getFirst()/getLast() (Java 21)
👉 Use this if you adopted SequencedCollection accessors on lists or deques.
Java 21 getFirst()/getLast() throw NoSuchElementException on empty collections just like pop(). Guard with isEmpty(), or stay with peekFirst()/pollFirst().
if (!seq.isEmpty()) {
var newest = seq.getLast();
}📋 Version Notes
Identical behavior; NoSuchElementException carries no message when thrown by iterator implementations.
SequencedCollection adds getFirst()/getLast() which also throw NoSuchElementException when empty; peekFirst()/pollFirst() remain null-safe.
🛡️ How to Prevent This Next Time
Treat next(), remove() and element() as licensed moves: each requires a prior successful guard. Prefer enhanced-for and streams so iterator bookkeeping stays inside the compiler.