Skip to main content

🧠 Java Production Trap I’ll Never Forget – Let’s Talk About ConcurrentModificationException

 

πŸ’¬ “When I was a fresher, I used to wonder: what things should I never do in Java?”

That curiosity led me to explore tons of weird bugs and war stories people had faced in production. Some were scary, some silly — but all of them were super valuable to understand deeply.

Today, I’m sharing one such issue I came across.

⚠️ P.S. I didn’t write this code — but I sure learned a lot digging into what went wrong and how it was fixed. πŸ˜…

This post is for the curious devs out there — especially freshers who want to learn the "why" behind Java's behaviors, not just memorize "what to do".


πŸ’₯ The Problem: ConcurrentModificationException in a Simple Loop

πŸ“Environment:
This happened in a Spring Boot microservice deployed on OpenShift (Linux-based). Interestingly, it didn’t throw any error in the developer's local Windows machine during testing. But in prod? BOOM. πŸ’£

Here’s the actual buggy code snippet:


List<String> users = getActiveUsers(); // Fetch from DB for (String user : users) { if (user.startsWith("temp_")) { users.remove(user); // 🚨 This caused the exception } }

And here’s the ugly surprise from the logs:


Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)

❓My Doubt: “Why ‘Concurrent’ if I'm Not Using Threads?”

That word "Concurrent" was super misleading to me back then.
I wasn’t using any threads! It was just a simple for-loop and a list.

So I started digging:
What does this error really mean? When and why does it happen?


πŸ”¬ Root Cause Analysis (RCA)

Here’s the twist: even though it looks like you’re looping normally, Java’s for-each loop actually uses an Iterator under the hood.

Let’s break it down:

  • Java’s ArrayList maintains an internal mod count (modCount) to track structural changes.

  • When you iterate using for (item : list), it’s essentially doing this:


Iterator<String> it = users.iterator(); while (it.hasNext()) { String user = it.next(); // Your logic here }
  • Now, when you do users.remove(user), you're modifying the list directly, while the iterator is still running.

  • The iterator notices that the list changed without using its own remove() method — and throws ConcurrentModificationException as a fail-fast mechanism.


πŸ› ️ The Fix: Remove Safely Without Angering the Iterator Gods πŸ˜…

Approach 1: Use Iterator Properly


List<String> users = getActiveUsers(); Iterator<String> it = users.iterator(); while (it.hasNext()) { String user = it.next(); if (user.startsWith("temp_")) { it.remove(); // ✅ Safe removal } }

Approach 2: Use Java Streams to Filter


List<String> users = getActiveUsers() .stream() .filter(u -> !u.startsWith("temp_")) .collect(Collectors.toList());

🧠 Why Does This Work?

Using it.remove() is safe because the iterator knows about the removal and updates its internal state. It’s like you politely told the iterator:

“Hey, I’m removing this one — please adjust your internal counter accordingly.” ☕

In the stream version, you’re not modifying the original list at all — you’re creating a new filtered list, which avoids the mutation problem entirely.



"Whaaatttt, then what does iterator.remove() do internally, and why does that work while list.remove() doesn’t?"


πŸ”¬ What Happens Internally When iterator.remove() is Called?

Time to go inside Java’s ArrayList and Iterator πŸš€

πŸ“Œ Internals of ArrayList.iterator()

Java’s ArrayList uses an internal class called Itr as its iterator.

Let’s see its key variables:


int cursor; // index of next element to return int lastRet = -1; // index of last element returned, -1 if none int expectedModCount = modCount; // used for CME detection

πŸ“Œ What .remove() does:

When you call iterator.remove(), it internally calls the parent list’s remove method, but also updates internal state safely.


public void remove() { if (lastRet < 0) throw new IllegalStateException(); ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; // ✅ sync back the modCount }

πŸ’‘ So it does 3 crucial things:

  1. Removes element at correct position (lastRet)

  2. Moves the cursor properly so next iteration continues smoothly

  3. Updates expectedModCount to match modCount so that CME is NOT thrown


🧠 So Why list.remove() (outside iterator) fails?

Let’s say:


List<String> list = new ArrayList<>(List.of("A", "B", "C")); Iterator<String> it = list.iterator();
  1. Internally:

    • modCount = 0

    • expectedModCount = 0

  2. When you call list.remove(1) directly:

    • modCount++ → modCount = 1

    • But expectedModCount in the iterator stays 0

  3. On next it.next(), it checks:


if (modCount != expectedModCount) throw new ConcurrentModificationException();

πŸ’₯ BOOM! That's your exception.



🧊 Final Analogy Time

Think of iterator like a security guard:

  • If you try to remove someone from the party (direct list.remove), the guard freaks out 😱 — CME!

  • But if the guard (iterator.remove) removes them himself, all is cool 😎



😡 Why Didn't It Fail in Local?

This is a classic gotcha.

  • Small dataset in local? Iterator might never throw.

  • JVM behavior or optimizations on your machine might not enforce the check immediately.

  • But in production (large dataset + different JVM or OS), the fail-fast behavior gets enforced reliably.

That’s why the same code works locally but crashes in prod — the worst kind of bug. πŸ˜–


🧩 What I Learned

  • ConcurrentModificationException is not about threads — it’s about modifying collections during iteration in an unsafe way.

  • Always respect the Iterator pattern if you’re doing removals inside a loop.

  • And never assume something is safe just because it runs locally!


🎨 Wrapping Up

I didn’t write this buggy code — but man, I’m glad I found it while exploring “what not to do” in Java. It helped me understand how iteration really works under the hood.

If you're just starting your Java journey, I highly recommend doing this too:

Don’t just learn “how” to fix something — learn why it broke in the first place.

It’ll make you 10x stronger as a dev. πŸš€


πŸ”š Next Weird Bug coming soon!


Got questions or thoughts? Drop them in the comments or connect with me — I love geeking out over stuff like this. πŸ˜„

Comments

Popular posts from this blog

πŸ” Is final Really Final in Java? The Truth May Surprise You 😲

πŸ’¬ “When I was exploring what to do and what not to do in Java, one small keyword caught my eye — final . I thought it meant: locked, sealed, frozen — like my fridge when I forget to defrost it.”   But guess what? Java has its own meaning of final… and it’s not always what you expect! πŸ˜… Let’s break it down together — with code, questions, confusion, jokes, and everything in between. 🎯 The Confusing Case: You Said It's Final... Then It Changed?! 🫠 final List<String> names = new ArrayList <>(); names.add( "Anand" ); names.add( "Rahul" ); System.out.println(names); // [Anand, Rahul] 🀯 Hold on... that’s final , right?! So how on earth is it still changing ? Time to dive deeper... 🧠 Why Is It Designed Like This? Here’s the key secret: In Java, final applies to the reference , not the object it points to . Let’s decode this like a spy mission πŸ•΅️‍♂️: Imagine This: final List<String> names = new ArrayList <>(); Be...

🎒 Java Loops: Fun, Fear, and ForEach() Fails

πŸŒ€ Oops, I Looped It Again! — The Ultimate Java Loop Guide You Won't Forget “I remember this question from one of my early interviews — I was just 2 years into Java and the interviewer asked, ‘Which loop do you prefer and why?’” At first, I thought, “Duh! for-each is cleaner.” But then he grilled me with cases where it fails. 😡 That led me to explore all loop types, their powers, and their pitfalls. Let’s deep-dive into every major Java loop with examples &  real-world guidance so you'll never forget again. πŸ” Loop Type #1: Classic For Loop — “The Old Reliable” ✅ When to Use: You need an index You want to iterate in reverse You want full control over loop mechanics ✅ Good Example: List<String> names = List.of("A", "B", "C"); for (int i = 0; i < names.size(); i++) { System.out.println(i + ": " + names.get(i)); } πŸ”₯ Reverse + Removal Example: List<String> item...

🧡 Virtual Threads in Java — The Ultimate Guide with Diagrams, Code & Interview Qs!

πŸš€ “How are Virtual Threads different from Thread Pools?” 😡 “Are they OS threads or JVM threads?” πŸ™ƒ “Should I still use CompletableFuture?” 🀯 “How do I even use them in real-time microservices?” 🧠 What are Virtual Threads? Virtual Threads (introduced in Java 21 as stable πŸŽ‰) are lightweight threads managed by the JVM instead of the OS kernel. πŸ‘‰ They look like normal threads, but don’t hog OS resources like traditional threads. 🧠 What is the OS Kernel? πŸ›️ OS Kernel = The Brain of the Operating System It’s the core part of your OS (Windows, Linux, Mac) that: Manages memory 🧠 Schedules threads πŸ•’ Talks to hardware πŸ’» Handles I/O operations πŸ“¨ When you create a traditional thread in Java, the JVM asks the OS Kernel to create a real OS-level thread. πŸ–Ό️ Imagine This... ┌───────────────────────────┐ │ Your Java Application │ └────────────┬──────────────┘ │ ...