Stackify is now BMC. Read theBlog

How to Specify and Handle Exceptions in Java

By: HitSubscribe
  |  June 7, 2024
How to Specify and Handle Exceptions in Java

Errors happen all the time in the software world. It might be an invalid user input, an external system not responding, or 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 exception handling mechanism that 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:

  • What is an Exception in Java?
  • What is Exception Handling?
  • 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
  • Why do we need Exception Handling?

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

What is an Exception in Java?

In Java, an exception is an event that arises during the execution of a program and disrupts its normal flow. It typically signals an erroneous situation or conditions the program shouldn’t encounter. Exceptions are objects that represent these error conditions. They can be generated by the Java runtime system or by the application code itself.

Exception handling is a mechanism that allows a program to respond to exceptional conditions (like runtime errors) gracefully without crashing

What is Exception Handling?

Exception handling is a mechanism that allows a program to respond to exceptional conditions (like runtime errors) gracefully without crashing. In Java, this is achieved using a combination of the `try`, `catch`, `finally`, and `throw` statements. The primary objective of exception handling is to provide a means to detect and report an error so that the program can either handle it and continue or terminate gracefully.

Java Exception Handling: Common Terminology

Call Stack

The call stack is the ordered list of methods that have been called to get to a specific method. In the context of this post, these methods 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 has the wrong format and can’t be converted into a number.

As with 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 of 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 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 disrupts 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 detail about exception handling in this post’s How to Handle an Exception section.

Types of Exceptions in Java

In Java, exceptions are categorized into two main types:

Checked Exceptions

These are the exceptions that are checked at compile time. If a method does not handle a checked exception using a try-catch block or does not declare it using the `throws` keyword, it will give a compilation error. Examples include `IOException`, `SQLException`, etc.

Unchecked Exceptions

Unchecked exceptions are not checked at compile-time, but they are checked at runtime. These are also called runtime exceptions. Examples include `ArithmeticException`, `NullPointerException`, etc.

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 method, allowing a more straightforward resource cleanup process.

Try-Catch-Finally

That is the classical approach to handling 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 that 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 that 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 not handled by 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 MyBusinessExceptionand 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 fundamental. 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)

You should use a more advanced implementation in an actual application. You can show an error message to the user and request a different input, or you can write a record into the work log of your batch process. Sometimes, it is okay 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 handles an exception. It is, therefore, an excellent 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 final block provides an excellent option to prevent 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 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 must also 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. The handling of the IOException, which might be thrown while closing the FileInputStream, doesn’t require a nested try-catch statement. A catch block of the try-with-resource statement now handles it.

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 must 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 handle or specify the exception themselves.

If you want to indicate that a method throws an unchecked exception, 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. As you might guess, it isn’t easy to provide a recommendation that’s a good fit for all use cases.

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

  1. Can you 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 better to specify it. That allows the caller of your class to implement the handling as it fits the current use case.

A program might crash unexpectedly without proper exception handling, leading to a poor user experience

Why do we need Exception Handling?

There are several reasons why exception handling is crucial:

User Experience: A program might crash unexpectedly without proper exception handling, leading to a poor user experience.

Resource Management: Properly handling exceptions can ensure that resources (like files or network connections) are closed or released correctly even when an error occurs.

Error Diagnosis: Exception handling provides detailed information about where and why a particular error occurred, making it easier to diagnose and fix.

Robustness: It allows the creation of robust programs to handle unexpected situations during runtime.

The `throw` and `throws` keywords in Java

In Java, exception handling isn’t just about catching exceptions and signaling them. This is where the `throw` and `throws` keywords come into play.

`throw` keyword:

The `throw` keyword explicitly throws an exception from a method or any block of code. We can throw either checked or unchecked exceptions using the `throw` keyword. An instance of the exception class follows the `throw` keyword.

Example:

void checkAge(int age) {
    if(age < 18) {
        throw new ArithmeticException("Access denied - You must be at least 18 years old.");
    }
    else {
        System.out.println("Access granted - You are old enough!");
    }
}

`throws` keyword:

The `throws` keyword is used to declare exceptions. It doesn’t throw an exception. It specifies that the method can throw the exceptions mentioned, allowing the caller of the method to handle or propagate them. If a method is capable of causing an exception that it does not handle, it must specify this behavior to the caller.

Example:

void myMethod() throws IOException {
    FileReader file = new FileReader("C:\\test\\a.txt");
    BufferedReader fileInput = new BufferedReader(file);
    // rest of the code
}

Summary

OK, that’s all about Java exception handling for now. I will get into more detail 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.

It would help if you used a checked exception for all exceptional events that the application can expect and handle. 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.

It would help to 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 the same way you handle or specify a checked exception.

Retrace APM with code profiling can collect exceptions directly from Java without any code changes! Try Prefix, Stackify’s free code profiler, to write better code on your workstation. 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]