How to Specify and Handle Exceptions in Java

By: Thorben
  |  February 16, 2024
java exception handling featured image

Errors happen all the time in the software world. It might be an invalid user input or an external system that is not responding, or it’s a simple programming error. In all these situations, the errors occur at runtime and the application needs to handle them. Otherwise, it crashes and can’t process further requests. Java provides a powerful mechanism which allows you to handle the exceptional event where it occurred or in one of the higher methods in the call stack.

In this article, we’ll cover the following topics:

  • Common Terminology of Java Exception Handling
  • Checked And Unchecked Exceptions in Java
  • How to Handle an Exception
  • How to Specify an Exception
  • How to Know Whether to handle or Specify an Exception

Before we get into the details of Java’s exception handling, we need to define a few terms.

Java Exception Handling: Common Terminology

Call Stack

The call stack is the ordered list of methods that had been called to get to a specific method. In the context of this post, these are the methods which were called to get to the method in which the error occurred.

Let’s have a look at an example. Method1 calls method2 which calls method3. The call stack now contains the following three entries:

  • method3
  • method2
  • method1

Exception Class and Hierarchy

The exception class identifies the kind of error that occurred. A NumberFormatException, for example, gets thrown when a String had the wrong format and couldn’t be converted into a number.

As every Java class, the exception class is part of an inheritance hierarchy. It has to extend java.lang.Exception or one of its subclasses.

The hierarchy is also used to group similar kinds of errors. An example for that is the IllegalArgumentException. It indicates that a provided method argument is invalid and it’s the superclass of the NumberFormatException.

You can also implement your own exception classes by extending the Exception class or any of its subclasses. The following code snippet shows a simple example of a custom exception.

public class MyBusinessException extends Exception {
	private static final long serialVersionUID = 7718828512143293558L;
	public MyBusinessException() {
		super();
	}
	public MyBusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
		super(message, cause, enableSuppression, writableStackTrace);
	}
	public MyBusinessException(String message, Throwable cause) {
		super(message, cause);
	}
	public MyBusinessException(String message) {
		super(message);
	}
	public MyBusinessException(Throwable cause) {
		super(cause);
	}
}

Exception Object

An exception object is an instance of an exception class. It gets created and handed to the Java runtime when an exceptional event occurred that disrupted the normal flow of the application. This is called “to throw an exception” because in Java you use the keyword “throw” to hand the exception to the runtime.

When a method throws an exception object, the runtime searches the call stack for a piece of code that handles it. I will get into more details about exception handling in the How to Handle an Exception section of this post.

Checked And Unchecked Exceptions in Java

Java supports checked and unchecked exceptions. You can use them in similar ways, and there are quite a few discussions about when to use which kind of exception. But that’s beyond the scope of this post. For now, let’s just follow the approach explained in Oracle’s Java Tutorial.

You should use checked exceptions for all exceptional events that you can anticipate and that a well-written application should be able to handle. A checked exception extends the Exception class. A method that throws a checked exception or that calls a method that specifies a checked exception needs to either specify or handle it.

Unchecked exceptions extend the RuntimeException. You should use them for internal errors that you can’t anticipate and that, most often, the application can’t recover from. Methods can but don’t need to handle or specify an unchecked exception. Typical examples that throw unchecked exceptions are:

  • the missing initialization of a variable which results in a NullPointerException or
  • the improper use of an API that causes an IllegalArgumentException

How to Handle an Exception

Java provides two different options to handle an exception. You can either use the try-catch-finally approach to handle all kinds of exceptions. Or you can use the try-with-resource approach which allows an easier cleanup process for resources.

Try-Catch-Finally

That is the classical approach to handle an exception in Java. It can consist of 3 steps:

  • a try block that encloses the code section which might throw an exception,
  • one or more catch blocks that handle the exception and
  • a finally block which gets executed after the try block was successfully executed or a thrown exception was handled.

The try block is required, and you can use it with or without a catch or finally block.

The Try Block

Let’s talk about the try block first. It encloses the part of your code that might throw the exception. If your code throws more than one exception, you can choose if you want to:

  • use a separate try block for each statement that could throw an exception or
  • use one try block for multiple statements that might throw multiple exceptions.

The following example shows a try block which encloses three method calls.

public void performBusinessOperation() {
	try {
		doSomething("A message");
		doSomethingElse();
		doEvenMore();
	}
	// see following examples for catch and finally blocks
}
public void doSomething(String input) throws MyBusinessException {
	// do something useful ...
	throw new MyBusinessException("A message that describes the error.");
}	
	
public void doSomethingElse() {
	// do something else ...
}
	
public void doEvenMore() throws NumberFormatException{
	// do even more ...
}

As you can see in the method definitions, only the first and the third method specify an exception. The first one might throw a MyBusinessException, and the doEvenMore method might throw a NumberFormatException.

In the next step, you can define one catch block for each exception class you want to handle and one finally block. All checked exceptions that are not handled by any of the catch blocks need to be specified.

The Catch Block

You can implement the handling for one or more exception types within a catch block. As you can see in the following code snippet, the catch clause gets the exception as a parameter. You can reference it within the catch block by the parameter name.

public void performBusinessOperation() {
	try {
		doSomething("A message");
		doSomethingElse();
		doEvenMore();
	} catch (MyBusinessException e) {
		e.printStackTrace();
	} catch (NumberFormatException e) {
		e.printStackTrace();
	}
}

The previous code sample shows two catch blocks. One to handle the MyBusinessException and one to handle the NumberFormatException. Both blocks handle the exceptions in the same way. Since Java 7, you can do the same with just one catch block.

public void performBusinessOperation() {
	try {
		doSomething("A message");
		doSomethingElse();
		doEvenMore();
	} catch (MyBusinessException|NumberFormatException e) {
		e.printStackTrace();
	}
}

The implementation of the catch blocks in the previous examples is very basic. I just call the printStackTrace method which writes the class, message and call stack of the exception to system out.

com.stackify.example.MyBusinessException: A message that describes the error.
	at com.stackify.example.TestExceptionHandling.doSomething(TestExceptionHandling.java:84)
	at com.stackify.example.TestExceptionHandling.performBusinessOperation(TestExceptionHandling.java:25)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

In a real application, you might want to use a more advanced implementation. You can, for example, show an error message to the user and request a different input or you could write a record into the work log of your batch process. Sometimes, it might even be ok to catch and ignore the exception.

And in production, you also need to monitor your application and its exception handling. That’s where Retrace and its error monitoring capabilities become very helpful.

The Finally Block

The finally block gets executed after the successful execution of the try block or after one of the catch blocks handled an exception. It is, therefore, a good place to implement any cleanup logic, like closing a connection or an InputStream.

You can see an example of such a cleanup operation in the following code snippet. The finally block will be executed, even if the instantiation of the FileInputStream throws a FileNotFoundException or the processing of the file content throws any other exception.

FileInputStream inputStream = null;
try {
	File file = new File("./tmp.txt");
	inputStream = new FileInputStream(file);
	
	// use the inputStream to read a file
	
} catch (FileNotFoundException e) {
	e.printStackTrace();
} finally {
	if (inputStream != null) {
		try {
			inputStream.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

As you’ve seen, the finally block provides a good option to prevent any leaks. And before Java 7, it was a best practice to put all cleanup code into a finally block.

Try-With-Resource

That changed when Java 7 introduced the try-with-resource statement. It automatically closes all resources that implement the AutoCloseable interface. And that is the case for most Java objects that you need to close.

The only thing you need to do to use this feature is to instantiate the object within the try clause. You also need to handle or specify all exceptions that might be thrown while closing the resource.

The following code snippet shows the previous example with a try-with-resource statement instead of a try-catch-finally statement.

File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
	// use the inputStream to read a file
} catch (FileNotFoundException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
}

As you can see, the try-with-resource statement is a lot easier to implement and read. And the handling of the IOException, which might be thrown while closing the FileInputStream, doesn’t require a nested try-catch statement. It is now handled by a catch block of the try-with-resource statement.

How to Specify an Exception

If you don’t handle an exception within a method, it will be propagated within the call stack. And if it’s a checked exception, you also need to specify that the method might throw the exception. You can do that by adding a throws clause to the method declaration. As a result, all calling methods need to either handle or specify the exception themselves.

If you want to indicate that a method might throw an unchecked exception, you may specify this as well.

public void doSomething(String input) throws MyBusinessException {
	// do something useful ...
	// if it fails
	throw new MyBusinessException("A message that describes the error.");
}

Handle or Specify an Exception

As so often, it depends on the use case if you should handle or specify an exception. And as you might guess, it’s difficult to provide a recommendation that’s a good fit for all use cases.

In general, you need to ask yourself the following questions:

  1. Are you able to handle the exception within your current method?
  2. Can you anticipate the needs of all users of your class? And would handling the exception fulfill these needs?

If you answer both questions with yes, you should handle the exception within your current method. In all other situations, it’s most likely better to specify it. That allows the caller of your class to implement the handling as it fits the current use case.

Summary

OK, that’s all about Java exception handling for now. I will get into more details about best practices and common errors in future posts of this series.

As you’ve seen, Java offers you two general types of exceptions: The checked and the unchecked exception.

You should use a checked exception for all exceptional events that can be expected and handled by the application. You need to decide if you want to handle it within a method or if you specify it. You can handle it with a try-catch-finally or a try-with-resource block. If you decide to specify the exception, it becomes part of the method definition, and the exception needs to be specified or handled by all calling methods.

You should use an unchecked exception for internal errors that can’t be anticipated. You are not required to handle or specify this kind of exception, but you can do it in the same way as you handle or specify a checked exception.

When using Retrace APM with code profiling, you can collect exceptions directly from Java, without any code changes! To write better code on your workstation, try Prefix, Stackify’s free code profiler. Prefix works with .NET, Java, PHP, Node.js, Ruby, and Python.

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]