Catch me if you can: Java’s Exceptional Rollercoaster

Being a Java Developer is fun and easy they say. Java is strong, type-safe and secure they say. But all this magic fades when you encounter your first Exception. And you can’t get used to those, trust me! You are spending countless hours debugging, reading StackOverflow, creating tests to catch it and when you are finally done, you are so exhausted that you can’t celebrate the victory. I’ve prepared a list of Exceptions that I encountered throughout my career.
Let me know if you’ve checked off all these exceptions on your list, or perhaps encountered even more!
NullPointerException (NPE)
Yes, that infamous NPE! It indicates that the application tried to use an object reference that has a null value. In other words, it happens when you call a method of the object that is null.
For example, arrayList.get(i) where arrayList is still null, new ArrayList<>() was never called. Or string.toUpperCase() where the string is still null.
This is probably the most common runtime exception in Java, and while it’s not “unexpected,” it’s certainly annoying to debug when not provided with a clear context.
How many sleepless nights does NPE cost to you?
ArrayIndexOutOfBoundsException
I think this one is very well known to those who just started learning Java. You always run into it when start calculating arrays, solving matrix tasks, and algorithmic problems.
I can read it in your eyes “Should it be n or n-1 this time?”…
So, it is thrown to indicate that you’ve attempted to access an array element using an index that’s outside the array’s bounds. Rest assured, everyone faced it. But this is an easy one to train your nerves on!
OutOfMemoryError: PermGen Space
This one that is special, you hardly can encounter it nowadays! So, it is a gem in my collection :)
Before Java 8, the Permanent Generation (PermGen) space was a fixed-size portion of the heap used for class metadata and statics. Continual deployment without restarting could fill up this space. This error indicates that the JVM ran out of memory in the PermGen section, often due to too many classes or too large classes being loaded.
OutOfMemoryError: Java heap space
When your application’s objects consume all the allocated heap space and the garbage collector can’t free up any more space, Java throws this error. This can arise from memory leaks or just underestimating the amount of memory your application needs.
I’ll tell you a story. I once supported a legacy application that was loading the whole database in its cache, aggregated it by some rules, and gave away the aggregated data to UI. The amount of data was growing. Steadily and not slowly at all :) This application was giving out this error like every day. literally, no kidding. It was so annoying that I killed it. No regrets, it was almost dead already, so it was an act of mercy :)
StackOverflowError
Yes, this one is classic! And become very popular thanks to well-known Q&A programmers' resource.
In a nutshell, this is usually the result of a poorly designed recursive call that never meets a terminating condition. It’s the JVM’s way of saying that the application recursion has gone too deep. And even though it is scary and frustrating, it is much easier to fix and catch than NPE.
For example, imagine that a client sends a deeply nested JSON, either maliciously or due to a bug on their side.
Your recursive logic continues to dive deeper into the nested structure without adequate checks in place.
public class JsonProcessor {
public void processJson(Map<String, Object> jsonMap) {
// Process the current level of JSON
// ... logic to process the current map ...
// Then recursively process nested maps (representing nested JSON objects)
for (Object value : jsonMap.values()) {
if (value instanceof Map) {
processJson((Map<String, Object>) value); // Recursive call
}
}
}
}If a client sends a deeply nested JSON, such as:
{
"data": {
"data": {
"data": {
"data": {
// ... and this goes on for thousands of levels ...
}
}
}
}
}Your service might throw a StackOverflowError because of the deep recursion caused by processing this JSON structure
ClassNotFoundException
This exception is thrown when an application tries to load a class at runtime using methods like Class.forName() or ClassLoader.loadClass(), and the class is not found in the classpath.
One of the most common scenarios where developers might encounter this exception is when trying to connect to a database using JDBC.
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "user", "password");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}In the code above, if the MySQL JDBC driver jar isn’t included in the classpath, then the call to Class.forName() will throw a ClassNotFoundException.
NoClassDefFoundError
This error is a bit more weird. It is thrown when the JVM or the ClassLoader instance tries to load a particular class and the required .class file was present during compile-time (so the code compiled successfully) but is absent during runtime.
How this is even possible, one may ask.
Well, this is Java. Everything is possible! I’ll show you the magic. Follow my hands:
You’re building a large application with multiple dependencies, and you use a build tool like Maven or Gradle. During development, you add a library (let’s call it LibraryA) to your build file, and you write code that uses a class (ClassA) from this library.
Everything compiles just fine. Now, before deployment, someone decides to “optimize” the build and mistakenly removes LibraryA from the final packaged application, thinking it's not needed. Or maybe LibraryA is still there, but due to some configuration issues, it's not correctly loaded. When your code runs and tries to use ClassA, boom: NoClassDefFoundError.
Another classic scenario is when you have two versions of the same library in the classpath. One might override the other, leading to situations where the class might be loaded from a different version than the one you expected, potentially causing NoClassDefFoundError if there are significant differences between those versions.
UnsupportedClassVersionError
This happens when you compile your Java application on a newer version of Java but then try to run it on an older version. The UnsupportedClassVersionError is like the universe's way of telling Java developers, "Hey, time matters!"
Imagine you’re a Java developer working on a web service for an e-commerce platform. You’re using some of the fancy new features introduced in Java 17, and you’ve set up your local development environment to use JDK 17. Everything compiles beautifully, your unit tests are all green, and the web service runs perfectly on your machine.
The application’s .jar or .war file is then created, and you hand it over to your operations team to deploy it onto the production server. The ops team, being cautious and perhaps a bit behind on updates, is still using JDK 11 on the production server. They deploy your application, and boom! The server throws an UnsupportedClassVersionError.
Why did that happen?
Your application was compiled with JDK 17, resulting in a bytecode version that is not understood by the older JVM (JDK 11) on the production server. The JVM on the production server is essentially saying:
“I don’t understand this new version of bytecode.”
ConcurrentModificationException
This occurs when one tries to modify a collection while iterating over it using an iterator, and it’s not being handled properly. Java collections are fail-fast, which means any concurrent modification after the creation of the iterator will result in this exception.
Imagine you’re building a chat application where users can join various chat rooms. For simplicity, let’s consider a global chat room that keeps a list of online users:
List<User> onlineUsers = new ArrayList<>();Now, let’s say you have a background task or thread that checks for inactive users every few minutes and removes them from the onlineUsers list to keep it updated. At the same time, there's another thread or process that's sending a broadcast message to all online users, iterating over the same list:
// Background task for removing inactive users
new Thread(() -> {
for (User user : onlineUsers) {
if (isInactive(user)) {
onlineUsers.remove(user);
}
}
}).start();
// Sending broadcast message to all users
for (User user : onlineUsers) {
sendMessageToUser("Hello everyone!", user);
}In the above scenario, there’s a clear race condition. If the background task is iterating through the onlineUsers list and removes an inactive user, while at the same time the broadcasting loop is sending messages, you'll get a ConcurrentModificationException.
IllegalMonitorStateException
This is thrown when a thread attempts to wait on an object’s monitor or to notify other threads waiting on an object’s monitor without owning the specified monitor. It’s a more nuanced error often encountered during multithreading tasks.
Imagine you’re a Java developer working on a distributed system, and you’ve been assigned a task to implement a custom blocking queue. This queue would allow producers to add items and consumers to take items, but with a twist: if the queue is empty, consumers should wait until there’s something to consume, and if the queue is full, producers should wait until there’s space to produce.
You decide to implement this using Java’s locks (synchronized methods and blocks) and the wait() and notify() mechanism.
public class CustomBlockingQueue<T> {
private final int capacity;
private final List<T> queue = new ArrayList<>();
public CustomBlockingQueue(int capacity) {
this.capacity = capacity;
}
public void add(T item) {
if (queue.size() == capacity) {
// Intention is to make the producer thread wait if the queue is full.
this.wait();
}
queue.add(item);
this.notify(); // Notify any waiting consumers that there's a new item.
}
public T take() {
if (queue.isEmpty()) {
// Intention is to make the consumer thread wait if the queue is empty.
this.wait();
}
T item = queue.remove(0);
this.notify(); // Notify any waiting producers that there's space now.
return item;
}
}When you try to execute the above code with multiple threads, you’ll quickly encounter the IllegalMonitorStateException when calling this.wait() or this.notify().
The methods wait(), notify(), and notifyAll() are only allowed to be called on an object when the current thread holds the monitor for that object, which is obtained using a synchronized method or block. In the code above, you've mistakenly called wait() and notify() outside of a synchronized context.
I will make an exception here and provide you with a way to avoid it so you won’t think that the code above is correct:
public synchronized void add(T item) throws InterruptedException {
while (queue.size() == capacity) {
this.wait();
}
queue.add(item);
this.notify();
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
T item = queue.remove(0);
this.notify();
return item;
}Note the addition of synchronized to the method signatures and the usage of while instead of if to recheck the condition after waking up.
AssertionError
This exception is thrown when the assert statement fails. While it's a part of the Java language, it can sometimes catch you off-guard, especially if you aren't expecting assertions to be enabled in a particular runtime environment.
Imagine you’re a Java developer for a flourishing e-commerce platform. You’ve developed a backend system that manages inventory for products. Every time a product is sold, the inventory count for that product decreases. For safety, you’ve made sure that a product’s inventory count never goes below zero by placing checks in the code.
However, during development, you decided to add an assertion as a secondary safeguard:
public class InventoryManager {
private Map<String, Integer> productInventory = new HashMap<>();
public void reduceInventory(String productId, int count) {
int currentCount = productInventory.getOrDefault(productId, 0);
// Ensure the inventory doesn't go below zero.
if (currentCount < count) {
currentCount = 0;
} else {
currentCount -= count;
}
// A safeguard assertion.
assert currentCount >= 0: "Inventory went negative for the product: " + productId;
productInventory.put(productId, currentCount);
}
}Unexpected Crash
All seems well during the testing and development phases. However, one day after deploying a new version of your backend system, you receive reports that the service crashed with an AssertionError.
After some investigation, you realize that for some obscure reason, the JVM in the production environment had been started with assertions enabled (using the -ea flag).
An edge case had occurred where two threads tried to reduce the inventory for the same product at the same moment, causing a race condition and leading to the product’s inventory count going negative for a brief moment. The assertion caught this anomaly and threw the AssertionError, which wasn't gracefully handled, causing the service to crash.
Having seen all these exceptions over the years, I can assure you that, while they might be frustrating at the moment, each one provides a valuable learning opportunity.
Java is an awesome language that I am in love with. All those exceptions are an integral part of her character and saying that, I accept all her imperfections that just make her more perfect in my eyes!
If you enjoyed reading my article, please consider buying me a coffee 💗 and stay tuned to more articles about Java, tech, and AI 👩🏻💻
