Skip to main content
Logging

Java Exception Handling: A Tutorial on How to Catch Errors and Improve Performance

Rafal Kuć Rafal Kuć on

Error handling is often a significant part of the application code. You might use conditionals to handle cases where you expect a certain state and want to avoid erroneous execution – for example, division by zero. However, there are cases where conditions are not known or are well hidden because of third party code. In other cases, code is designed to create an exception and throw it up the stack to allow for the graceful handling of an error at a higher level.

Using exceptions is very handy; they let you separate the business logic from the application code for handling errors. However, exceptions are neither free or cheap. Throwing an exception requires the JVM to perform operations like gathering the full stack trace of the method calls and passing it to the method that is responsible for handling the exception. In other words, it requires additional resources compared to a simple conditional.

No matter how expensive the exceptions are, they are invaluable for troubleshooting issues. Correlating exceptions with other data like metrics and various application logs can help with resolving your problems fast and in a very efficient manner. This is precisely why Sematext makes it very easy to correlate logs and metrics.

What Is an Exception in Java?

An Exception is an event that causes your normal Java application flow to be disrupted. Exceptions can be caused by using a reference that is not yet initialized, dividing by zero, going beyond the length of an array, or even JVM not being able to assign objects on the heap.

In Java, an exception is an Object that wraps the error that caused it and includes information like:

  • The type of error that caused the exception with the information about it.
  • The stack trace of the method calls.
  • Additional custom information that is related to the error.

Various Java libraries throw exceptions when they hit a state of execution that shouldn’t happen – from the standard Java SDK to the enormous amounts of open source code that is available as a third-party library.

Exceptions can be created automatically by the JVM that is running our code or explicitly our code itself. We can extend the classes that are responsible for exceptions and create our own, specialized exceptions that handle unexpected situations. But keep in mind that throwing an exception doesn’t come for free. It is expensive. The larger the call stack the more expensive the exception.

Exception Class Hierarchy

Java is an object-oriented language, and thus, every class will extend the java.lang.Object. The same goes for the Throwable class, which is the base class for the errors and exceptions in Java. The following picture illustrates the class hierarchy of the Throwable class and its most common subclasses:

The Difference Between an Error and an Exception

Before going into details about the Exception class, we should ask one fundamental question: What is the difference between an Error and an Exception in Java?

To answer the question, let’s look at Javadocs and start from the Throwable class. The Throwable class is the mother of all errors – the superclass of all errors and exceptions that your Java code can produce. Only objects that are instances of the Throwable class or any of its subclasses can be thrown by the code running inside the JVM or can be declared in the methods throw clause. There are two main implementations of the Throwable class.

An Error is a subclass of Throwable that represents a serious problem that a reasonable application should not try to catch. The method does not have to declare an Error or any of its subclasses in its throws clause for it to be thrown during the execution of a method. The most common subclasses of the Error class are OutOfMemoryError and StackOverflowError classes. The first one represents JVM not being able to create a new object; we discussed potential causes in JVM garbage collector. The second error represents an issue when the application recurses too deeply.

The Exception and its subclasses, on the other hand, represent situations that an application should catch and try to handle. There are lots of implementations of the Exception class that represent situations that your application can gracefully handle. The FileNotFoundException, SocketException, or NullPointerException are just a few examples that you’ve probably come across when writing Java applications.

Types of Java Exceptions

There are multiple implementations of the Exception class in Java. They come from the Java Development Kit itself, but also from various libraries and applications that you might be using when writing your own code. We will not discuss every Exception subclass that you can encounter, but there are two main types that you should be aware of – the checked and unchecked, aka runtime exceptions. Let’s look into them in greater detail.

Checked Exceptions

The checked exceptions are the ones that implement the Exception class and not the RuntimeException. They are called checked exceptions because the compiler verifies them during the compile-time and refuses to compile the code if such exceptions are not handled or declared. Such exceptions are usually used to notify the user that a method can result in a state that needs to be handled. For example, the FileNotFoundException is an example of such an exception.

In order for a method or constructor to throw a checked exception it needs to be explicitly declared:

public CheckedExceptions() throws FileNotFoundException {
  throw new FileNotFoundException("File not found");
}

public void throwsExample() throws FileNotFoundException {
  throw new FileNotFoundException("File not found");
}

You can see that in both cases we explicitly have the throws clause mentioning the FileNotFoundException that can be thrown by the execution of the constructor of the throwsExample method. Such code will be successfully compiled and can be executed.

On the other hand, the following code will not compile:

public void wrongThrowsExample() {
  throw new FileNotFoundException("File not found");
}

The reason for the compiler not wanting to compile the above code will be raising the checked exception and not processing it. We need to either include the throws clause or handle the exception in the try/catch/finally block. If we were to try to compile the above code as is the following error would be returned by the compiler:

/src/main/java/com/sematext/blog/java/CheckedExceptions.java:18: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
    throw new FileNotFoundException("File not found");
    ^

Unchecked Exceptions / Runtime Exceptions

The unchecked exceptions are the second type of exceptions in Java. The unchecked exceptions in Java are the ones that implement the RuntimeException class. Those exceptions can be thrown during the normal operation of the Java Virtual Machine. Unchecked exceptions do not need to be declared in the method throws clause in order to be thrown. For example, this block of code will compile and run without issues:

public class UncheckedExceptions {
  public static void main(String[] args) {
    UncheckedExceptions ue = new UncheckedExceptions();
    ue.run();
  }

  public void run() {
    throwRuntimeException();
  }

  public void throwRuntimeException() {
    throw new NullPointerException("Null pointer");
  }
}

We create an UncheckedExceptions class instance and run the run method. The run method calls the throwRuntimeException method which creates a new NullPointerException. Because the NullPointerException is a subclass of the RuntimeException we don’t need to declare it in the throws clause.

The code compiles and propagates the exception up to the main method. We can see that in the output when the code is run:

Exception in thread "main" java.lang.NullPointerException: Null pointer
        at com.sematext.blog.java.UncheckedExceptions.throwRuntimeException(UncheckedExceptions.java:14)
        at com.sematext.blog.java.UncheckedExceptions.run(UncheckedExceptions.java:10)
        at com.sematext.blog.java.UncheckedExceptions.main(UncheckedExceptions.java:6)

This is exactly what we expected.

The most common subclasses of the RuntimeException that you probably saw already are ClassCastException, ConcurrentModificationException, IllegalArgumentException, or the everyone’s favorite NullPointerException.

Let’s now look at how we throw exceptions in Java.

Throwing Exceptions

An Exception in Java can be thrown by using the throw keyword and creating a new Exception or re-throwing an already created exception. For example, the following very naive and simple method creates an instance of the File class and checks if the file exists. If the file doesn’t exist the method throws a new IOException:

public File openFile(String path) throws IOException {
  File file = new File(path);
  if (!file.exists()) {
    throw new IOException(String.format("File %s doesn't exist", path));
  }
  // continue execution of the business logic
  return file;
}

You can also re-throw an Exception that was thrown inside the method you are executing. This can be done by the try-catch block:

public class RethrowException {
  public static void main(String[] args) throws IOException {
    RethrowException exec = new RethrowException();
    exec.run();
  }

  public void run() throws IOException {
    try {
     methodThrowingIOE();
    } catch (IOException ioe) {
      // do something about the exception
      throw ioe;
    }
  }

  public void methodThrowingIOE() throws IOException {
    throw new IOException();
  }
}

You can see that the run method re-throws the IOException that is created in the methodThrowingIOE. If you plan on doing some processing of the exception, you can catch it as in the above example and throw it further. However, keep in mind that in most cases, this is not a good practice. You shouldn’t catch the exception, process it, and push it up the execution stack. If you can’t process it, it is usually better to pack it into a more specialized Exception class, so that a dedicated error processing code can take care of it. You can also just decide that you can’t process it and just throw it:

public class RethrowException {
  public static void main(String[] args) throws IOException {
    RethrowException exec = new RethrowException();
    exec.run();
  }

  public void run() throws IOException {
    try {
     methodThrowingIOE();
    } catch (IOException ioe) {
      throw ioe;
    }
  }

  public void methodThrowingIOE() throws IOException {
    throw new IOException();
  }
}

There are additional cases, but we will discuss them when talking about how to catch exceptions in Java.

How Do You Handle Exceptions in Java?

We already know that Exceptions in Java are there to signal the abnormal state of the application execution. We also know that they exist so that we can deal with the state of the application that is not normal and continue the execution of the business code. Processing the exception, catching it, or even throwing it further up is what we call handling of an exception in Java.

In our application, we don’t want to just throw the created exception to the top of the call stack, for example, to the main method. That would mean that each and every exception that is thrown would crash the application and this is not what should happen. Instead, we want to handle exceptions, at least the ones that we can deal with, and either help fixing the problem or fail gracefully.

To see how we can deal with exceptions in Java, let’s discuss the following exception handling methods.

Try-catch Block

The first thing that you can do with code that is supposed to generate an exception is to catch it. The exception can be either checked or unchecked. In the first case, the compiler will tell you which kind of exceptions need to be caught or defined in the method throws clause for the code to compile. In the case of unchecked exceptions, we are not obligated to catch them. We don’t have to catch them, but it may be a good idea, at least in some cases. For example, there is a DOMException or DateTimeException that indicate issues that you can gracefully handle.

The code that might generate an exception should be placed in the try block which should be followed by the catch block. For example, let’s look at the code of the following openFileReader method:

public Reader openFileReader(String filePath) {
  try {
    return new FileReader(filePath);
  } catch (FileNotFoundException ffe) {
    // tell the user that the file was not found
  }
  return null;
}

The method tries to create a FileReader class instance. The constructor of that class can throw a checked FileNotFoundException if the file provided as the argument doesn’t exist. We could just include the FileNotFoundException in the throws clause of our openFileReader method or catch it in the catch section just like we did. In the catch section, we can do whatever we need to get a new file location, create a new file, and so on.

If we would like to just throw it further up the call stack the code could be further simplified:

public Reader openFileReaderWithThrows(String filePath) throws FileNotFoundException {
  return new FileReader(filePath);
}

This passes the responsibility of handling the FIleNotFoundException to the code that calls it.

Multiple Catch Block

We are not limited to a single catch block in the try-catch block. We can have multiple try-catch blocks. Let’s say that we have a method that lists more than a single exception in its throws clause, like this:

public void readAndParse(String file) throws FileNotFoundException, ParseException {
  // some business code
}

In order to compile and run this we need multiple catch blocks to handle each of the listed exceptions:

public void run(String file) {
  try {
    readAndParse(file);
  } catch (FileNotFoundException ex) {
    // do something when file is not found
  } catch (ParseException ex) {
    // do something if the parsing fails
  }
}

Such code organization can be used when dealing with multiple exceptions that need to be handled differently. Hypothetically, the above code could ask for a new file location when the FileNotFoundException happens and inform the user of the issues with parsing when the ParseException happens.

There is one more thing we should mention when it comes to multiple catch blocks. You need to remember that the order of the catch blocks matters. If you have a more specialized exception in the catch block after the more general exception the more specialized catch block will never be executed. Let’s look at the following example:

public void runTwo(String file) {
  try {
    readAndParse(file);
  } catch (Exception ex) {
    // this block will catch all exceptions
  } catch (ParseException ex) {
    // this block will not be executed
  }
}

Because the first catch block catches the Exception the catch block with the ParseException will never be executed. That’s because the ParseException is a subclass of the Exception.

Catching Multiple Exceptions

To process multiple exceptions with the came logic, you can list them all inside a single catch block. Let’s redo the above example with the FileNotFoundException and the ParseException to use a single catch block:

public void runSingleCatch(String file) {
  try {
    readAndParse(file);
  } catch (FileNotFoundException | ParseException ex) {
    // do something when file is not found
  }
}

You can see how easy it is – you can connect multiple exceptions together using the | character. If any of these exceptions is thrown in the try block the execution will continue inside the catch block.

The Finally Block

In addition to try and the catch blocks, there is one additional section that may come in handy – the finally block. The finally block is the last section in your try-catch-finally expression and will always get executed – either after the try or after the catch. It will be executed even if you use the return statement in the try block, which by the way, you shouldn’t do.

Let’s look at the following example:

public void exampleOne() {
  FileReader reader = null;
  try {
    reader = new FileReader("/tmp/somefile");
    // do some processing
  } catch (FileNotFoundException ex) {
    // do something
  } finally {
    if (reader != null) {
      try {
        reader.close();
      } catch (IOException ex) {
        // do something
      }
    }
  }
}

You can see the finally block. Keep in mind that you can have at most one finally block present. The way this code will be executed is as follows:

  1. The JVM will execute the try section first and will try to create the FileReader instance.
  2. If the /tmp/somefile is present and readable the try block execution will continue.
  3. If the /tmp/somefile is not present or is not readable the FileNotFoundException will be raised and the execution of the code will go to the catch block.
  4. After 2) or 3) the finally block will be executed and will try to close the FileReader instance if it is present.

The finally section will be executed even if we modify the try block and include the return statement. The following code does exactly the same as the one above despite having the return statement at the end of the try block:

public void exampleOne() {
  FileReader reader = null;
  try {
    reader = new FileReader("/tmp/somefile");
    // do something
    return;
  } catch (FileNotFoundException ex) {
    // do something
  } finally {
    if (reader != null) {
      try {
        reader.close();
      } catch (IOException ex) {
        // do something
      }
    }
  }
}

So, why and when to use the finally block? Well, it is a perfect candidate for cleaning up resources, like closing objects, calculating metrics, including logs about operation completion, and so on. Learn more about logging in Java and the best practices you should follow to get the most out of your Java application logs.

The Try-With-Resources Block

The last thing when it comes to handling exceptions in Java is the try-with-resources block. The idea behind that Java language structure is opening one on more resources in the try section that will be automatically closed at the end of the statement. That means that instead of including the finally block we can just open the resources that we need in the try section and rely on the Java Virtual Machine to deal with the closing of the resource.

For the class to be usable in the try-with-resource block it needs to implement the java.lang.AutoCloseable, which includes every class implementing the java.io.Closeable interface. Keep that in mind.

An example method that reads a file using the FileReader class which uses the try-with-resources might look as follows:

public void readFile(String filePath) {
  try (FileReader reader = new FileReader(filePath)) {
    // do something
  } catch (FileNotFoundException ex) {
    // do something when file is not found
  } catch (IOException ex) {
    // do something when issues during reader close happens
  }
}

Of course, we are not limited to a single resource and we can have multiple of them, for example:

public void readFiles(String filePathOne, String filePathTwo) {

  try (
      FileReader readerOne = new FileReader(filePathOne);
      FileReader readerTwo = new FileReader(filePathTwo);
  ) {
    // do something
  } catch (FileNotFoundException ex) {
    // do something when file is not found
  } catch (IOException ex) {
    // do something when issues during reader close happens
  }
}

One thing that you have to remember is that you need to process the exceptions that happen during resources closing in the catch section of the try-catch-finally block. That’s why in the above examples, we’ve included the IOException in addition to the FileNotFoundException. The IOException may be thrown during FileReader closing and we need to process it.

User-defined Exceptions

The Throwable and Exception are Java classes and so you can easily extend them and create your own exceptions. Depending on if you need a checked or unchecked exception you can either extend the Exception or the RuntimeException class. To give you a very simple example on how such code might look like, have a look at the following fragment:

public class OwnExceptionExample {
  public void doSomething() throws MyOwnException {
    // code with very important logic
    throw new MyOwnException("My Own Exception!");
  }

  class MyOwnException extends Exception {
    public MyOwnException(String message) {
      super(message);
    }
  }
}

In the above trivial example we have a new Exception implementation called MyOwnException with a constructor that takes a single argument – a message. It is an internal class, but usually you would just move it to an appropriate package and start using it in your application code.

Java Anti-Patterns

As with everything, there are a few bad practices related to handling exceptions in Java. Let’s discuss them so that you can avoid them.

Hiding Exceptions

Don’t hide exceptions unless you don’t care about the error. Process them, log them, or just print them to the console. Avoid doing things you can see in the following code:

try {
  FileReader reader = new FileReader(filePath);
  // some business code
} catch (FileNotFoundException ex) {}

As you can see in the code above the FileNotFoundException is hidden and we don’t have any idea that the creation of the FileReader failed. Of course, the code that runs after the creation of the FileReader would probably fail if it were to operate on that. Now imagine that you are catching exceptions like this:

try {
  FileReader reader = new FileReader(filePath);
  // some business code
} catch (Exception ex) {}

You would catch lots of exceptions and completely ignore them all. The least you would like to do is fail and log the information so that you can easily find the problem when doing log analysis or setting up an alert in your log centralization solution. Of course, you may want to process the exception, maybe do some interaction with external systems or the user itself, but for sure not hide the problem.

If you really don’t want to deal with an exception you can just print it to the log or error stream, for example:

try {
  FileReader reader = new FileReader(filePath);
  // some business code
} catch (Exception ex) {
  ex.printStackTrace();
}

Or even better, if you are using a centralized logging solution just do the following to store the error log there:

try {
  FileReader reader = new FileReader(filePath);
  // some business code
} catch (Exception ex) {
  LOG.error("Error during FileReader creation", ex);
}

Overusing Exceptions

This may not be true for every situation and for sure makes the code a bit uglier, but if you want to squeeze every bit of performance from your code, try to avoid exceptions when a simple comparison is enough. Look at the following method:

public int divide(int dividend, int divisor) {
  try {
    return dividend / divisor;
  } catch (ArithmeticException ex) {
    // do nothing
  }
  return -1;
}

It tries to divide two numbers and catches the ArithmeticException in case of an error. What kind of error can happen here? Well, division by zero is the perfect example. If the divisor argument is 0 the code will throw an exception, catch it in order to avoid failure in the code and continue returning -1. This is not the perfect way to go – throwing an exception has a performance penalty – we talk about it later in the blog post. In such cases, you should check the divisor argument and allow the division only if it is not equal to 0, just like this:

public int divide(int dividend, int divisor) {
  if (divisor != 0) {
    return 10 / divisor;
  }
  return -1;
}

The performance of throwing an exception and checking is noticeable. Keep that in mind when writing your code that handles exceptions and situations that should not be allowed.

Using Return Statement in The Finally Block

The next thing you should avoid is using the return statement in the finally block of your code. Have a look at the following code fragment:

public class ReturnInFinally {
  public static void main(String[] args) {
    ReturnInFinally app = new ReturnInFinally();
    app.example();
    System.out.println("Ended without error");
  }

  public void example() {
    try {
      throw new NullPointerException();
    } finally {
      return;
    }
  }
}

If you were to run it, it would end up printing the Ended without error to the console. But we did throw the NullPointerException, didn’t we? Yes, but that would be hidden because of our finally block. We didn’t catch the exception and according to Java language Exception handling specification, the exception would just be discarded. We don’t want something like that to happen, because that would mean that we are hiding issues in the code and its execution.

Using Throw Statement in The Finally Block

A very similar situation to the one above is when you try to throw a new Exception in the finally block. Again, the code speaks louder than a thousand words, so let’s have a look at the following example:

public class ThrowInFinally {
public static void main(String[] args) {
ThrowInFinally app = new ThrowInFinally();
app.example();
}

public void example() {
try {
throw new RuntimeException("Exception in try");
} finally {
throw new RuntimeException("Exception in finally");
}
}
}

If you were to execute the above code the sentence that will be printed in the error console would be the following one:

Exception in thread "main" java.lang.RuntimeException: Exception in finally
  at com.sematext.blog.java.ThrowInFinally.example(ThrowInFinally.java:13)
  at com.sematext.blog.java.ThrowInFinally.main(ThrowInFinally.java:6)

Yes, it is true. The RuntimeException that was thrown in the try block will be hidden and the one that was thrown in the finally block will take its place. It may not seem big, but have a look at the code like this:

public void example() throws Exception {
  FileReader reader = null;
  try {
    reader = new FileReader("/tmp/somefile");
  } finally {
    reader.close();
  }
}

Now think about the execution of the code and what happens in a case where the file specified by the filePath is not available. First, the FileReader constructor would throw a FileNotFoundException and the execution would jump to the finally block. Once it gets there it would try to call the close method on a reference that is null. That means that the NullPointerException would be thrown here and the whole example method would throw it. So practically, the FileNotFoundException would be hidden and thus the real problem would not be easily visible that well.

Troubleshooting Java Exceptions with Sematext

Handling Exceptions properly in your Java code is important. Equally important is being able to use them for troubleshooting your Java applications. With Sematext Cloud you can find the most frequent errors and exceptions in your Java application logs, create custom dashboards for your team, and set up real-time alerts and anomaly detection to notify you of issues that require your attention. Can you also correlate errors, exceptions, and other application logs with your application performance metrics. You can use Sematext not only to monitor your Java applications, but also to monitor Elasticsearch, Tomcat, Solr, Kafka, Nginx, your servers, processes, databases, even your packages, and the rest of your infrastructure. With service auto-discovery and monitoring everything is just a click away 🙂

Performance Side Effects of Using Exceptions

We’ve mentioned that using exceptions in Java doesn’t come for free. Throwing an exception in Java requires the JVM to fill up the whole call trace, list each method that comes with it, and pass it further to the code that will finally catch and handle the exception. That’s why you shouldn’t use exceptions unless it is really necessary.

Let’s look at a very simple test. We will run two classes – one will do the division by zero and catch the exception that is caused by that operation. The other code will check if the divisor is zero before throwing the exception. The mentioned code is encapsulated in a method.

The code that uses an exception looks as follows:

public int divide(int dividend, int divisor) {
  try {
    return dividend / divisor;
  } catch (Exception ex) {
    // handle exception
  }
  return -1;
}

The code that does a simple check using a conditional looks as follows:

public int divide(int dividend, int divisor) {
  if (divisor != 0) {
    return 10 / divisor;
  }
  return -1;
}

One million executions of the first method take 15 milliseconds, while one million executions of the second method take 5 milliseconds. So you can clearly see that there is a large difference in the execution time when running the code with exceptions and when using a simple conditional to check if the execution should be allowed. Of course, keep in mind that this comparison is very, very simple, and doesn’t reflect real-world scenarios, but it should give you an idea of what to expect when crafting your own code.

The test execution time will differ depending on the machine, but you can run it on your own if you would like by cloning this Github repository.

The Bottom Line

Even though exceptions in Java are not free and have a performance penalty they are invaluable for handling errors in your applications. They help in decoupling the main business code from the error handling code making the whole code cleaner and easier to understand. Using Exceptions wisely will make your code look good, be extensible, maintainable, and fun to work with. Use them without falling into the anti-patterns and log everything they tell you into your centralized logging solution so that you can get the most value out of any thrown and exceptions.