How to handle the 10 most common exceptions in Java
Learn about the most common exceptions in Java, what they mean, and how to solve them. We'll cover NullPointerException, NumberFormatException, and more.
Jul 17, 2024 • 11 Minute Read
In this article, we’ll cover the ten most common exceptions in Java, their causes, and their solutions. These exceptions account for over 97% of all Java exceptions. Once you know how to handle them, you’ll be ready to tackle nearly any Java error that comes your way.
Table of contents
What is the structure of exceptions in Java?
Before covering each exception, it’s important to know their overall structure. An exception in Java has as many as four elements:
A type (like IOException)
A message (like “too many open files”)
A stack trace (the list of calls from main onward that led to the line of code that triggered the exception)
A caused-by exception (which means the top-level exception is wrapping an earlier, lower-level exception)
Here is a simple exception:
Exception java.lang.IllegalArgumentException: item quantity must be a number
at io.jzheaux.pluralsight.DeliController.orderSandwich (DeliController.java:45)
// …
Caused by java.lang.NumberFormatException: For input string: " 3"
at NumberFormatException.forInputString (NumberFormatException.java:67)
at Integer.parseInt (Integer.java:647)
at Integer.parseInt (Integer.java:777)
at io.jzheaux.pluralsight.DeliController.orderSandwich (DeliController.java:39)
// …
In this stack trace, you can see that the exception type is IllegalArgumentException. Its message is “item quantity must be a number.” The stack trace shows that line 39 in DeliController gave Integer an invalid value. You can also see that NumberFormatException is the underlying cause.
Finding the underlying cause often means looking down the stack trace a few lines to get to your application code. This is because the exception often occurs in the Java or framework APIs that you are using.
10 common exceptions in Java and how to solve them
1. NullPointerException
By far the most common exception is the notorious NullPointerException. Java throws this exception whenever your application references a null variable.
There are obvious instances like the following:
Soccer ball = null;
ball.shoot(); // will throw NullPointerException
But there are also more subtle ones like unboxing a wrapper type:
Boolean willVote = null;
if (willVote) { // will throw NullPointerException
Or like when code assumes non-nullness in the parameters or return values it receives:
void parseDocument(Document doc) {
doc.getElements(); // will throw NullPointerException if the document passed is null
}
String lookupElement(Document doc) {
Element element = doc.findElement(“span”);
return element.getValue(); // will throw NullPointerException if the element returned is null
}
A NullPointerException usually stems from one of two concerns:
The value is invalid, and you should examine the upstream conditions, or
The value is unexpected, and you should guard its downstream usage
Invalid values
Let’s look at an example. It's not reasonable for a caller to expect parseDocument to work with a null document. In that case, it’s best to find out why the value is null and repair that.
You can also consider throwing a more informative exception. A common choice is IllegalArgumentException when the responsibility lies with the caller. You can also state this at compile time by using @NonNull and @NonNullApi annotations:
void parseDocument(@NonNull Document doc) {
Assert.notNull(doc, “doc cannot be null”);
doc.getElements();
}
Integrated development environments (IDEs) also analyze code based on these annotations, providing additional benefits.
Valid values
As for lookupElement, it may be reasonable for doc to not have a span element. In that case, it’s better to use a guard or sentinel value:
@Nullable String lookupElement(Document doc) {
Element element = doc.findElement(“span”);
if (element == null) {
return null;
}
return element.getValue();
}
Or you can use Optional or follow the Null Object pattern instead.
2. NumberFormatException
Java throws NumberFormatException when an application tries to convert a String into a number and fails. Here’s a simple example:
Integer.parseInt(“ 123 “); // throws NumberFormatException because of the whitespace
Often, this is caused by not sanitizing inputs from loosely-typed external systems. For example, you may have entries in a database for postal codes whose column is a VARCHAR instead of INT. Or, you may be taking input from an end user who accidentally mistyped their entry.
It’s uncommon that a method coercing input to a number also supports non-numeric values. Given that, it’s more robust and secure to fail so that the data is fixed at the source.
You can handle the failure in a few ways:
Propagate the exception. This ensures a quick fix to the issue. But this doesn't always provide enough context.
Wrap the exception. Wrapping in an IllegalArgumentException allows you to give more context to the caller.
Test against a regular expression. A good general practice is to validate all inputs before use. Consider validating before parsing, like so:
static final Pattern isPostalCode = Pattern.compile(“\\d{5}”);
String postalCode = readFromRequest(request);
if (!isPostalCode.matches(postalCode)) {
errors.add(“postal code must be 5 digits”);
} else {
int parsed = Integer.parseInt(postalCode);
//...
}
The above uses a regular expression to first see if the value is a five-digit number. If it passes, we already know that parseInt won't throw an exception. This approach can also avoid exceptions altogether, which offers performance benefits.
Frameworks can help here, too. For example, when building a REST API, Spring MVC handles this exception for you.
3. IllegalArgumentException and IllegalStateException
IllegalArgumentException and IllegalStateException have useful opposing semantics. Generally speaking, an IllegalArgumentException says the responsibility lies with the caller. An IllegalStateException says it lies with the callee.
In other words, if you see an IllegalArgumentException, it’s conventionally the caller’s fault. If you see an IllegalStateException, it’s conventionally the callee’s fault.
For example, the Spring Framework provides an input validation API that does just this:
public void setJwtValidator(OAuth2TokenValidator<Jwt> validator) {
Assert.notNull(validator, “validator cannot be null”);
// …
}
Because the caller is passing in an invalid value, Spring’s Assert class throws an IllegalArgumentException. To repair it, follow the instructions outlined in the exception message.
One rather curious instance of IllegalArgumentException is when the Java runtime version is lower than the compiler version.
IllegalStateException can be more complicated since the state of the application might not be anyone’s fault. Read the error message carefully and proceed with your research.
4. RuntimeException and Exception
Sometimes you'll see blanket RuntimeException or Exception stack traces in logs.
A RuntimeException is commonly thrown to convert a checked exception into an unchecked one.
This is often to clean up API declarations. For example, you often see this when translating an IOException like this one:
try (InputStream in = resource.getInputStream()) {
// …
} catch (IOException ex) {
throw new RuntimeException(ex);
}
Even though RuntimeException is less informative, remember that exceptions can have a caused-by element. Because this code wraps IOException, both stack traces will be available.
Note that sometimes developers use Exception just to log a call trace like so:
new Exception(“two Josh Cummingses entered the store at the same time!”).printStackTrace();
In this case, the developer isn't throwing an exception. They simply want the stack trace to be logged as they investigate the cause of a given phenomenon.
5. ClassCastException
A ClassCastException occurs when your application casts one type to another and fails. You can see this one for yourself by trying to cast Object to anything else.
(Integer) new Object()
While the compiler catches many casting mistakes, it doesn’t catch all of them. And because of that, some of them need to wait until runtime.
The most common occurs when downcasting like so:
Collection<Unit> units = this.repository.findAll();
ArrayList<Unit> casted = (ArrayList<Unit>) units;
This should be avoided where possible. In this case, the code is taking an unnecessary risk due to the advantages of coding to interfaces.
You can address a ClassCastException in three ways:
Try operating without casting. It’s often better to code to an interface anyway, as this makes your code more flexible.
Try converting instead of casting. Sometimes you can convert the value you have into the value you want as this code does:
Collection<Unit> units = this.repository.findAll();
ArrayList<Unit> casted = new ArrayList<>(units);
3. Check before casting. As a best practice, if you’re going to cast, you should check first. Recent versions of Java improved the syntax. For example, you could downcast Unit to UnitImpl like this:
Unit unit = casted.iterator().next()
if (unit instanceof UnitImpl implementation) {
// …
} else {
// ...
}
This is nice because the code in the if block will only run when the cast succeeds. The else block allows you to provide alternative behavior.
6. ClassNotFoundException
In this section, we'll look at three related exceptions: ClassNotFoundException, MethodNotFoundException, and NoClassDefFoundError. These are often indications that the Java Runtime is seeing something different from what the compiler saw.
For example, you might be using a third-party library to parse JSON like this:
ObjectMapper mapper = new ObjectMapper();
mapper.parseJson(value);
But then at runtime, you see the following:
Exception java.lang.ClassNotFoundException: com.fasterxml.jackson.core.ObjectMapper
// …
This is likely because the jar containing ObjectMapper is not in your runtime classpath. So you’ll need to check how that could be missing from your deployment.
MethodNotFoundException is often a symptom of a slightly different, though related, problem. If there was a method at compile time, but not at runtime, then that may indicate that there’s a jar mismatch. You may be using a newer third-party jar locally while production is using an older version that doesn’t have that method. In this case, the solution is the same: check the deployment.
NoClassDefFoundError is slightly different. It typically means that the class failed to load. In my experience, this is most common when a class has a static initialization block that fails. You can find this by looking for <clinit> in the stack trace. This is a special method that the runtime calls to initialize a class during startup.
These can also be caused by the Reflection API. I recommend avoiding reflection in general since the compiler can’t help. If these exceptions happen and you’re using reflection, check for typos in your class and method names. Also, consider rigorous unit tests to cover what the compiler cannot.
7. OutOfMemoryError
An OutOfMemoryError is what it sounds like. If your application is using too much memory, you will see this exception.
This can be caused by the application legitimately needing more memory. In that case, you can increase the memory allocated to it through the -Xmx command line option. If you’re containerized, you may also need to update your container's memory limits.
This exception can also be caused by memory leaks, such as Map instances that grow forever without eviction. Look for collections of objects that can grow over time and see if you can rework them to clear out old items. Tools like JConsole can help.
Finally, this exception can be caused by loading an external complex object into memory all at once. This can happen in a few ways:
With Hibernate. When eagerly loading joins, a single call can create a complex query that hydrates a large, complicated object graph. The solution is often to lazily load or craft a SQL query directly.
With XML Parsing. It can also happen when loading complex XML objects. This was once the source of a famous hack called Billion Laughs.
With Collections. That specific XML problem is largely mitigated today by more secure Java XML APIs. That said, favor streaming data instead of pulling it all into memory at once. For example, you can query in Spring Data like so:
Collection<Value> values = repository.findAll();
Depending on the size of the table, this may unnecessarily put memory pressure on the system. You can mitigate this by using Stream or paging like this:
Stream<Value> values = repository.findAll();
In this way, the API gives you one value at a time.
8. StackOverflowError
One of the hardest exceptions to debug is a StackOverflowError. These can be quite tricky since they by definition are ones where the runtime can’t give you the full stack trace. The causing code may still exist in the stack trace, but sometimes you aren’t so lucky.
This is usually caused by some form of recursion. For example, your application may have a method calling itself erroneously. Or it may be a more complex set of classes that cyclically call each other.
By definition, these stack traces are very big. If the stack trace doesn't show the offending line, you can use the -Xss command-line parameter to extend its length. This can also be handy if you’re running inside of a framework with several layers of abstraction.
When something isn’t available in a stack trace, you can sometimes add logging to close in on the location. You can use these repeated logs to discover other parts of the code that are participating in the loop.
9. IOException
IOException is a parent exception for several kinds of runtime errors, including File I/O and Network I/O.
You can see one in action by trying to open a file that doesn’t exist:
new FileInputStream(“nonexistent.file”)
Or try connecting to a non-existent website:
new java.net.Socket("l2k3n4lk23n4l2.com", 80)
If the underlying system, like a spotty network connection, is flaky, an application can sometimes recover. Likewise, an app can recover when redundant systems are present and the app can cycle through them when one fails. In these cases, you may also see an UncheckedIOException wrapping the underlying IOException.
That said, an IOException usually means the application can’t recover, often due to misconfiguration or an unanticipated outage. Check the related filesystem and network and see whether it or the code needs to change.
10. SQLException
SQLException is also a parent exception. It encapsulates any error caused when communicating with a SQL database.
Like IOException, this exception may be thrown when connecting with a database. Most often, though, it indicates a problem with how the database rejected the SQL provided by the application. This may be due to a few things:
A syntax error,
A discrepancy in the table structure that the application is expecting, or
A discrepancy in the SQL version or dialect that the database supports
Databases aren’t always the most helpful at determining the exact problem. When investigating, consider turning up logging to see the exact SQL query in question.
SQLException is also thrown by the Java API for formulating and transmitting queries. For example, ResultSet throws it when you try to read a non-queried column. And PreparedStatement throws this exception when your application sets a non-existent bind variable:
Connection conn = …
PreparedStatement ps = conn.prepareStatement(“SELECT * FROM my_table WHERE id = ?”);
ps.setInt(1, 123);
ps.setInt(2, 456); // this will throw a SQLException since there isn’t a second bind variable (?) in the query
To fix it, correct your SQL statement, how you’re populating it, or how you’re reading the result.
Conclusion: Troubleshoot Java errors faster
In this article, you saw the ten most common exceptions in Java and how I’ve addressed them over the years. Understanding what each exception type means as well as their common causes and solutions can help you get to a better solution faster.
Use the stack trace to gather context and pinpoint the error’s source. And remember that sometimes the responsibility to fix the error lies with the callee rather than the caller. Armed with this knowledge, you’re now better equipped to tackle Java errors head-on. Let’s go fix some bugs!
Keep learning: Boost your Java skills
Ready to level up your Java skills? Check out Pluralsight's dedicated Java learning paths written for tech professionals, by tech professionals:
If you're interested in Java, you may also want to check out these resources on the Spring framework, an open-source framework for building robust and scalable Java applications. Pluralsight offers paths on core Spring as well as Spring Security.