tree_title | description | last_modified |
---|---|---|
Exceptions |
The different kinds of exceptions in Java and some best practices for using them |
2022-10-06 15:58:58 UTC |
- Basics
- Checked versus unchecked exceptions
- Throwing and the
throws
declaration - Catching exceptions
- Cleaning up using try-with-resources
- Cleaning up using
finally
- Rethrowing and chaining
- Dealing with
NullPointerException
- Turning checked exceptions into unchecked exceptions
- Resources
Exception handling:
- A method can throw an exception indicating that something is wrong
- Exceptions automatically bubble up the call chain until they are handled (or, if they are not handled, they end up terminating the current thread)
- Great benefit when comparing to error codes which must be manually passed up the chain if needed
- Typically, exceptions include a stack trace, indicating where the exception occurred in the code and what the call chain looked like at the time
Hierarchy of exception classes in Java:
Classes:
Throwable
:- Common superclass for all Java exceptions
Error
:- Exceptions defined by the Java language that are thrown when something really bad happens that the code can normally not recover from by itself
- All unchecked (see below)
- Example:
OutOfMemoryError
, which occurs when Java is unable to allocate space to an object because there is no more memory available and garbage collection does not help. If you run into that one, the best you can generally do is exit the program.
Exception
:- All user-defined exceptions are subclasses of this one
- Exceptions deriving directly from this one are checked (see below) exceptions
RuntimeException
:- User-defined exceptions that are subclasses of this one are unchecked (see below) exceptions
Basic idea:
- Checked: Checked by compiler
- If your code can possibly throw a checked exception, the Java compiler requires you to either catch the exception or use a
throws
declaration to indicate that your code can throw the exception
- If your code can possibly throw a checked exception, the Java compiler requires you to either catch the exception or use a
- Unchecked: Not checked by compiler
Intention of this design:
- Use checked exceptions when there is a reasonable way of recovering from the failure during the execution of the program
- Example: a call to open a file and write to it, which can fail if the file does not exist. A reasonable way of recovering from this is trying a different filename.
- When using checked exceptions, the compiler forces you to to decide what to do in case such an exception occurs. This prevents you from forgetting that this kind of failure can actually happen.
- Use unchecked exceptions for errors that the program cannot reasonably recover from
- Example: exceptions indicating programming errors, like
NullPointerException
- Example: exceptions indicating programming errors, like
Alternative school of thought:
- Use checked exceptions if there is a reasonable way of recovering from the failure during the execution of the program and the possibility for failure is unavoidable (an example is a missing file: even if you checked for its existence before, the file can still have disappeared in the meantime)
- If the possibility for failure is unavoidable, it makes sense to force the caller to deal with the possible failure
- Use unchecked exceptions in all other cases.
- Evaluate these conditions at every level in the call chain.
Yet another different (and popular school of thought):
- Always use unchecked exceptions.
- Never use checked exceptions (unless you are maybe writing a very critical library).
- This is advertised in, amongst others, the Clean Code book by Robert C. Martin
- Reason: drawbacks of checked exceptions
- If you throw a checked exception from your code and the appropriate handler sits three levels higher in the call chain, you must declare the exception on every method in between
- This means that a single change to a small method somewhere deep in the program can require the signature of several higher-level methods to change, breaking encapsulation and coupling the handler of the exception to the code that generates it
- This actually negates a large benefit of exceptions, which is that you can decouple the code detecting a failure from the code handling the failure
Throwing unchecked exception:
public static boolean isNumberBetween(int number, int lower, int upper) {
if (lower > upper) {
throw new IllegalArgumentException(
"Lower bound cannot be higher than upper bound");
}
// ...
}
If code can throw a checked exception, a throws
clause is mandatory!
public void write(String text, String filePath) throws IOException {
// potentially throws checked IOException
Files.write(Paths.get(filePath), text.getBytes());
}
Throws and inheritance:
- If you override a method, you cannot throw more checked exceptions than the original
throws
clause specifies. Otherwise, you would break the contract of the method. - If a method does not have a
throws
clause, a method overriding it cannot throw any checked exceptions - Note that you can perfectly override a method declaring checked exceptions with a method not throwing any exceptions at all
Simple try-catch block:
try {
// code potentially throwing exception
} catch (TheExceptionClass ex) {
// code handling the exception
}
Can also have multiple catch statements, catching exceptions of different exception classes. They are evaluated top to bottom, so put more specific exception classes first. It is also possible to have a handler that can handle several exception classes.
try {
// code potentially throwing exception
} catch (ExceptionClass1 ex) {
// code handling the exception
} catch (ExceptionClass2 | ExceptionClass3 ex) {
// code handling the exception
}
Java offer a try-with-resources statement. In this form of try
statement, you can specify resources that should be closed automatically. The fact that a resource can be closed automatically is indicated by the fact that it implements the AutoCloseable
interface.
public void write(ArrayList<String> lines) throws FileNotFoundException {
try(PrintWriter out = new PrintWriter("output.txt")) {
for (String line: lines) {
this.possiblyGenerateException(line);
out.println(line);
}
}
}
When the code exits the try block, either because we finished processing the lines or an exception was thrown inside, the PrintWriter
will be closed automatically. It is also possible to provide multiple resources to a try-with-resources statement. In that case, the order in which they are closed is the reverse of the order in which they were provided.
A try-with-resources statement can also have catch clauses catching any exceptions occurring in the statement.
Note: it is possible that closing an AutoCloseable
throws an exception itself!
- If closing happened after normal execution of the
try
block, that exception is passed to the caller - If closing happened because of an exception happening inside the
try
block, the exception from thetry
block is passed to the caller and any exceptions generated by subsequently closing the resources will be attached as suppressed exceptions on that exception- When catching the exception that happened in the
try
block (the primary exception), you can access those suppressed exceptions by invoking thegetSuppressed()
method on the caught exception.
- When catching the exception that happened in the
public class ThrowsOnClose implements AutoCloseable {
@Override
public void close() {
throw new IllegalStateException("Closing");
}
}
try (ThrowsOnClose throwsOnClose = new ThrowsOnClose()) {
throw new IllegalArgumentException("Inside try");
} catch (Exception e) {
System.out.println(e.getMessage()); // Inside try
for (Throwable suppressed: e.getSuppressed()) {
System.out.println(suppressed.getMessage()); // Closing
}
}
Alternative to the try-with-resources statement: the finally
clause.
- After the
try
block and potentialcatch
blocks, you add afinally block
which will run after the rest of the try-catch statement has finished - Especially useful if you need to clean up something that is not an
AutoCloseable
. - Note: avoid throwing exceptions in the
finally
block!- If the code in the
try
block throws an exception and then thefinally
block throws an exception, the caller of the method will only see the exception from thefinally
block. The suppression mechanism we saw above only works with try-with-resources statements.
- If the code in the
Rethrowing:
- Catch the exception
- Do something useful with it (for example, logging it)
- Just throw the exception object that you caught
Chaining:
- Catch the exception
- Throw a new exception instead
- Original exception is typically included as the cause for the new exception
Chaining example:
try {
// set foreign key to parent in database
} catch (SQLException ex) {
throw new ParentNotFoundException(ex); // sets ex as cause
}
When the caller catches the ParentNotFoundException
, it can find the original exception using the getCause()
method.
In this case, ParentNotFoundException
has a constructor which allows passing the cause (this is considered good practice). If an exception class does not allow that, the cause for the exception can be set through the initCause()
method.
NullPointerException
is an unchecked exception indicating a programming error (we called a method on a null reference). The best way of dealing with NullPointerException
is to avoid it.
You can make failure with a NullPointerException
less likely by using the following set of rules for your code:
- Don’t return null from methods. Wherever possible, return a special case object (e.g. an empty list, which you can easily obtain from
Collections.emptyList()
, or an instance of a dedicated class that was intended for these cases). - Avoid passing null as a method parameter.
You can use null-safety annotations to let the compiler enforce these kinds of rules. For example, you could use the following null-safety annotations from the Spring framework org.springframework.lang
package:
@NonNull
can be used to explicitly mark a field, method parameter or return value as non-nullable@NonNullApi
can be used at package level to mark all method parameters and return values in the package as non-nullable by default@NonNullFields
can be used at package level to mark all fields in the package as non-nullable by default@Nullable
can be used to explicitly mark a field, method parameter or return value as nullable (useful for overriding@NonNullApi
and@NonNullFields
where needed)
The Objects
class has some convenient methods for preventing problems with null pointers. These can be used for checking arguments or preventing passing null.
Objects.requireNonNull()
throws aNullPointerException
if the argument is null and returns the argument itself otherwise. This does not prevent theNullPointerException
from being thrown. However, the benefit here is that we throw it where the source of the problem lies (null
being passed where it is not allowed) instead of at some later point in the code where we try to invoke a method on a null reference.Objects.requireNonNullElse()
returns either the first parameter or, if the first parameter is null, it returns the second parameter. It is quite similar to theCOALESCE
function in SQL.
public static void test(String name, String nickname) {
name = Objects.requireNonNull(name);
nickname = Objects.requireNonNullElse(nickname, "nick");
}
Something else that can help is the Optional
type. Wrapping the returned value in an Optional
type makes it clear to the caller of your method that there may be no actual value returned. It also allows the caller to deal with missing values in a nice way. See also Optional type (Java).
In some cases, you may find yourself getting a checked exception which you want to pass up the method chain as an unchecked exception (for example because you are overriding a method without a throws
declaration allowing the checked exception).
Simplest way to do this: chaining (see above)
- Catch checked exception
- Throw new unchecked exception with checked exception as the cause
More sneaky approach: trick the Java compiler into ignoring checked exceptions. Example implementation (taken from Core Java SE 9 for the Impatient):
public class Exceptions {
@SuppressWarnings("unchecked")
private static <T extends Throwable>
void throwAs(Throwable e) throws T {
throw (T) e; // erased to (Throwable) e
}
public static <V> V doWork(Callable<V> c) {
try {
return c.call();
} catch (Throwable ex) {
Exceptions.<RuntimeException>throwAs(ex);
return null;
}
}
}
Now, any checked exceptions thrown inside the doWork
method are ignored by the compiler.
public static void test() {
// error: unhandled FileNotFoundException
new PrintWriter("output.txt");
// ok
Exceptions.doWork(() -> new PrintWriter("output.txt"));
}
Although this is a pretty cool trick, it is not generally applicable in practice. Not only would it be confusing to catch an IOException
from a method that doesn’t declare it, but the compiler does not even allow you to do it.
public static void invokeTest() {
try {
test();
} catch (IOException ex) {
// error: unreachable catch block
}
}
public static void test() {
Exceptions.doWork(() -> new PrintWriter("output.txt"));
}
Some cases where this trick may make sense (although not necessarily good practice):
- Dealing with “impossible” exceptions
- Some methods in the standard library have a
throws
clause declaring checked exceptions, although there are some sets of arguments which are guaranteed to never throw an exception - If you know that you are passing such a set of arguments, you could use this trick to stop the compiler from bothering you about this impossible exception.
- Some methods in the standard library have a
- Creating a
Runnable
that can throw a checked exception in itsrun()
method (which does not declare any checked exceptions)- You can define custom error handling behavior for the thread (for both checked and unchecked exceptions) in the thread’s uncaught exception handler
Example of the Runnable
case:
public class TestRunnable implements Runnable {
@Override
public void run() {
Exceptions.doWork(() -> new PrintWriter("unwritableFile"));
}
}
Thread thread = new Thread(new TestRunnable());
thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
if (e instanceof IOException) {
System.out.println("Caught IOException");
}
}
});
thread.start(); // Caught IOException
- Core Java SE 9 for the Impatient (book by Cay S. Horstmann)
- Clean Code (book by Robert C. Martin)
- Unchecked Exceptions — The Controversy
- When to choose checked and unchecked exceptions
- Spring Null-Safety Annotations
- Project Lombok @SneakyThrows (throwing checked exceptions as unchecked)