Dive In Java Exception Handling

Amirhosein Gharaati
Javarevisited
Published in
12 min readJul 3, 2024

Why do we use exception handling?

Without handling exceptions, a healthy program may stop running altogether!

  1. We need to make sure that our code has a plan for when things go wrong.
  2. One benefit is the stack trace itself. Because of this stack trace, we can often pinpoint offending code without needing to attach a debugger.

Advantages

Separating Error-Handling Code from “Regular” Code

Consider the pseudo code method here that reads an entire file into memory:

readFile {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}

At first glance, this function seems simple enough, but it ignores all the following potential errors.

  • What happens if the file can’t be opened?
  • What happens if the length of the file can’t be determined?
  • What happens if enough memory can’t be allocated?
  • What happens if the read fails?
  • What happens if the file can’t be closed?

To handle such cases, the readFile function must have more code to do error detection, reporting, and handling. Here is an example of what the function might look like.

errorCodeType readFile {
initialize errorCode = 0;

open the file;
if (theFileIsOpen) {
determine the length of the file;
if (gotTheFileLength) {
allocate that much memory;
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) {
errorCode = -1;
}
} else {
errorCode = -2;
}
} else {
errorCode = -3;
}
close the file;
if (theFileDidntClose && errorCode == 0) {
errorCode = -4;
} else {
errorCode = errorCode and -4;
}
} else {
errorCode = -5;
}
return errorCode;
}

There’s so much error detection, reporting, and returning here. Worse yet, the logical flow of the code has also been lost.

It’s even more difficult to ensure that the code continues to do the right thing when you modify the method three months after writing it.

Exceptions enable you to write the main flow of your code and to deal with the exceptional cases elsewhere:

readFile {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) {
doSomething;
} catch (sizeDeterminationFailed) {
doSomething;
} catch (memoryAllocationFailed) {
doSomething;
} catch (readFailed) {
doSomething;
} catch (fileCloseFailed) {
doSomething;
}
}

Propagating Errors Up the Call Stack

Suppose that the readFile method is the fourth method in a series of nested method calls made by the main program:

method1 {
call method2;
}

method2 {
call method3;
}

method3 {
call readFile;
}

Suppose also that method1 is the only method interested in the errors that might occur within readFile.

Traditional error-notification techniques force method2 and method3 to propagate the error codes returned by readFile up the call stack until the error codes finally reach method1 .

method1 {
errorCodeType error;
error = call method2;
if (error)
doErrorProcessing;
else
proceed;
}

errorCodeType method2 {
errorCodeType error;
error = call method3;
if (error)
return error;
else
proceed;
}

errorCodeType method3 {
errorCodeType error;
error = call readFile;
if (error)
return error;
else
proceed;
}

A method can duck any exceptions thrown within it, thereby allowing a method farther up the call stack to catch it. Hence, only the methods that care about errors have to worry about detecting errors:

method1 {
try {
call method2;
} catch (exception e) {
doErrorProcessing;
}
}

method2 throws exception {
call method3;
}

method3 throws exception {
call readFile;
}

However, as the pseudocode shows, ducking an exception requires some effort on the part of the middleman methods. Any checked exceptions that can be thrown within a method must be specified in its throws clause.

Grouping and Differentiating Error Types

A method can write specific handlers that can handle a very specific exception. The FileNotFoundException class has no descendants, so the following handler can handle only one type of exception.

catch (FileNotFoundException e) {
...
}

A method can catch an exception based on its group or general type by specifying any of the exception’s superclasses in the catch statement. For example, to catch all I/O exceptions, regardless of their specific type, an exception handler specifies an IOException argument.

catch (IOException e) {
...
}

This handler will be able to catch all I/O exceptions, including FileNotFoundException, EOFException, and so on. You can find details about what occurred by querying the argument passed to the exception handler. For example, use the following to print the stack trace.

catch (IOException e) {
// Output goes to System.err.
e.printStackTrace();
// Send trace to stdout.
e.printStackTrace(System.out);
}

You could even set up an exception handler that handles any Exception with the handler here.

// A (too) general exception handler
catch (Exception e) {
...
}

In most situations, however, you want exception handlers to be as specific as possible. The reason is that the first thing a handler must do is determine what type of exception occurred before it can decide on the best recovery strategy.

In effect, by not catching specific errors, the handler must accommodate any possibility. Exception handlers that are too general can make code more error-prone by catching and handling exceptions that weren’t anticipated by the programmer and for which the handler was not intended.

Types of Exceptions

There are mainly two types of exceptions: checked and unchecked. An error is considered as the unchecked exception. However, according to Oracle, there are three types of exceptions namely:

  1. Checked Exception
  2. Unchecked Exception
  3. Error

1. Checked Exception

The classes that directly inherit the Throwable class except RuntimeException and Error are known as checked exceptions.

  • IOException
  • SQLException
  • ServletException

Checked exceptions are exceptions that the Java compiler requires us to handle. We have to either declaratively throw the exception up the call stack, or we have to handle it ourselves.

Oracle’s documentation tells us to use checked exceptions when we can reasonably expect the caller of our method to be able to recover.

Checked exceptions are checked at compile-time.

2. Unchecked Exception

The classes that inherit the RuntimeException are known as unchecked exceptions.

  • ArithmeticException
  • NullPointerException
  • ArrayIndexOutOfBoundsException
  • IllegalArgumentException

Unchecked exceptions are exceptions that the Java compiler does not require us to handle.

Simply put, if we create an exception that extends RuntimeException, it will be unchecked; otherwise, it will be checked.

And while this sounds convenient, Oracle’s documentation tells us that there are good reasons for both concepts, like differentiating between a situational error (checked) and a usage error (unchecked).

Unchecked exceptions are not checked at compile-time, but they are checked at runtime.

3. Error

Error is irrecoverable.

  • OutOfMemoryError
  • AssertionError
  • StackOverflowError

Errors represent serious and usually irrecoverable conditions like a library incompatibility, infinite recursion, or memory leaks.

And even though they don’t extend RuntimeException, they are also unchecked.

In most cases, it’d be weird for us to handle, instantiate or extend Errors. Usually, we want these to propagate all the way up.

Handling Exception In Java

throws

The simplest way to “handle” an exception is to rethrow it:

public int getPlayerScore(String playerFile)
throws FileNotFoundException {

Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
}

Because FileNotFoundException is a checked exception, this is the simplest way to satisfy the compiler, but it does mean that anyone that calls our method now needs to handle it too!

parseInt can throw a NumberFormatException, but because it is unchecked, we aren’t required to handle it.

try-catch

If we want to try and handle the exception ourselves, we can use a try-catch block. We can handle it by rethrowing our exception:

public int getPlayerScore(String playerFile) {
try {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException noFile) {
throw new IllegalArgumentException("File not found");
}
}

Or by performing recovery steps:

public int getPlayerScore(String playerFile) {
try {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch ( FileNotFoundException noFile ) {
logger.warn("File not found, resetting score.");
return 0;
}
}

try-with-resource

The try-with-resources statement is a try statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.

static String readFirstLineFromFile(String path) throws IOException {
try (FileReader fr = new FileReader(path);
BufferedReader br = new BufferedReader(fr)) {
return br.readLine();
}
}

Because the FileReader and BufferedReader instances are declared in a try-with-resource statement, they will be closed regardless of whether the try statement completes normally or abruptly

Java Exception Propagation

An exception is first thrown from the top of the stack and if it is not caught, it drops down the call stack to the previous method.

If not caught there, the exception again drops down to the previous method, and so on until they are caught or until they reach the very bottom of the call stack. This is called exception propagation.

Exception can be handled in any method in call stack either in the main() method, p() method, n() method or m() method.

By default, Checked Exceptions are not forwarded in calling chain (propagated).

Java Custom Exception

We can create our own exceptions that are derived classes of the Exception class.

Basically, Java custom exceptions are used to customize the exception according to user need.

Why use custom exceptions?

Following are few of the reasons to use custom exceptions:

  • To catch and provide specific treatment to a subset of existing Java exceptions.
  • Business logic exceptions: These are the exceptions related to business logic and workflow. It is useful for the application users or the developers to understand the exact problem.

Example

Imagine you created a custom REST client for API calls.

This is just an example. You can implement it in any other way:

public record ResponseEntity<T>(
Integer code,
Boolean successful,
String message,
T body
) {
public ResponseEntity(Response response, T body) {
this(
response.code(),
response.isSuccessful(),
response.message(),
body
);
}
}

public <T> ResponseEntity<T> send(Request request, Class<T> responseType) {
OkHttpClient client = new OkHttpClient();

try (Response response = client.newCall(request).execute()) {
if (response.body() == null) {
log.error("empty response body");
throw new EmptyResponseException("empty response body");
}

String responseBody = response.body().string();
if (!response.isSuccessful()) {
log.error("response was not successful: {}", responseBody);
throw new BadResponseException("response was not successful: %s".formatted(responseBody));
}

if (responseType == Void.class) {
return new ResponseEntity<>(response, null);
}

T entity = objectMapper.readValue(responseBody, responseType);
return new ResponseEntity<>(response, entity);

} catch (IOException e) {
log.error("error occurred while processing request: {}", e.getMessage());
throw new ClientException(e.getMessage());
}
}

There are 3 custom exceptions here:

EmptyResponseException, BadResponseException, and ClientException

All with extending RuntimeException:

public class BadResponseException extends RuntimeException {
public BadResponseException(String message) {
super(message);
}
}

In the send method, you have more control to treat with the behavior and response.

You can have your own exception with specific constructions and behavior.

You throw exception or print stack trace in the log. It is based in the project requirements and behaviors.

Questions

Why did the designers decide to force a method to specify all uncaught checked exceptions that can be thrown within its scope?

Any Exception that can be thrown by a method is part of the method's public programming interface.

Those who call a method must know about the exceptions that a method can throw so that they can decide what to do about them. These exceptions are as much a part of that method's programming interface as its parameters and return value.

If it’s so good to document a method’s API, including the exceptions it can throw, why not specify runtime exceptions too?

Runtime exceptions represent problems that are the result of a programming problem, and as such, the API client code cannot reasonably be expected to recover from them or to handle them in any way.

Such problems include

  • arithmetic exceptions, such as dividing by zero
  • pointer exceptions, such as trying to access an object through a null reference
  • indexing exceptions such as attempting to access an array element through an index that is too large or too small.

Why we should create custom RuntimeExceptions?

Runtime exceptions can occur anywhere in a program, and in a typical one they can be very numerous.

Having to add runtime exceptions in every method declaration would reduce a program’s clarity. Thus, the compiler does not require that you catch or specify runtime exceptions (although you can).

Generally speaking, do not throw a RuntimeException or create a subclass of RuntimeException simply because you don't want to be bothered with specifying the exceptions your methods can throw.

Where should I create checked or unchecked exceptions?

If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.

Anti-Patterns

Swallowing Exceptions

Now, there’s one other way that we could have satisfied the compiler:

public int getPlayerScore(String playerFile) {
try {
// ...
} catch (Exception e) {} // <== catch and swallow
return 0;
}

It doesn’t address the issue and it keeps other code from being able to address the issue, too.

In those cases that we are confident it will never happen, we should still at least add a comment stating that we intentionally ate the exception:

public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
// this will never happen
}
}

Another way we can “swallow” an exception is to print out the exception to the error stream simply:

public int getPlayerScore(String playerFile) {
try {
// ...
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}

It’d be better, though, for us to use a logger:

public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
logger.error("Couldn't load the score", e);
return 0;
}
}

One ideal thing we can do is throwing a custom related exception:

public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
throw new PlayerScoreException(e);
}
}

Using return in a finally Block

This is bad because, by returning abruptly, the JVM will drop the exception, even if it was thrown from by our code:

public int getPlayerScore(String playerFile) {
int score = 0;
try {
throw new IOException();
} finally {
return score; // <== the IOException is dropped
}
}

Using throw in a finally Block

This will “erase” the original exception from the try block, and we lose all of that valuable information:

public int getPlayerScore(String playerFile) {
try {
// ...
} catch ( IOException io ) {
throw new IllegalStateException(io); // <== eaten by the finally
} finally {
throw new OtherException();
}
}

Using throw as a goto

This is odd because the code is attempting to use exceptions for flow control as opposed to error handling.

public void doSomething() {
try {
// bunch of code
throw new MyException();
// second bunch of code
} catch (MyException e) {
// third bunch of code
}
}

Best Practices

Log errors, but be careful what you log

You should log the errors to save information about why really an error happened in the system.

However, you must ensure no protected data is written in the log files.

Don’t bury thrown exceptions

Don’t catch an exception and then do nothing with it. That’s known as burying an exception.

Buried exceptions makes troubleshooting Java applications extremely hard.

Use a global Exception handler

There will always be uncaught RuntimeExceptions that creep into your code.

Always include a global Exception handler to deal with any uncaught exceptions.

Throw early and handle exceptions late

As soon as an exception condition happens in your code, throw an Exception.

The function to catch exceptions should go toward the end of a method. This puts fewer catch blocks in your methods, and makes your code much easier to read and maintain

Don’t log and rethrow

Never do both. You should never log and then rethrow the same exception, as is done in the following example:

/* log and rethrow exception example */
try {
Class.forName("com.mcnz.Example");
} catch (ClassNotFoundException ex) {
log.warning("Class was not found.");
throw ex;
}

Doing so causes code duplication, and litters the log files with duplicate entries which makes it much more difficult to troubleshoot code.

Check for suppressed exceptions

The suppressed exception is a relatively new language feature that not all developers are aware of.

You can easily check this situation simply by querying for the suppressed exception’s existence, as is done in the example below:

try ( Door door = new Door() ) {
door.swing(); /* Throws the SwingException */
}
catch (Exception e) {
System.out.println("Primary Exception: " + e.getClass());
if (e.getSuppressed().length > 0) {
System.out.print("Suppressed Exception: " + e.getSuppressed()[0]);
}
}

Explicitly define exceptions in the throws clause

Lazy developers use the generic Exception class in the throws clause of a method.

Instead, always explicitly state the exact set of exception a given method might throw. This allows other developers to know the various error handling routines they can employ if a given method fails to execute properly.

--

--