Exception Handling in Java: A Complete Guide with Best and Worst Practices

Overview

Handling Exceptions in Java is one of the most basic and fundamental things a developer should know by heart. Sadly, this is often overlooked and the importance of exception handling is underestimated - it's as important as the rest of the code.

In this article, let's go through everything you need to know about exception handling in Java, as well as good and bad practices.

What is Exception Handling?

We are surrounded by exception handling in real-life on an everyday basis.

When ordering a product from an online shop - the product may not be available in stock or there might occur a failure in delivery. Such exceptional conditions can be countered by manufacturing another product or sending a new one after the delivery failed.

When building applications - they might run into all kinds of exceptional conditions. Thankfully, being proficient in exception handling, such conditions can be countered by altering the flow of code.

Why use Exception Handling?

When building applications, we're usually working in an ideal environment - the file system can provide us with all of the files we request, our Internet connection is stable and the JVM can always provide enough memory for our needs.

Sadly, in reality, the environment is far from ideal - the file cannot be found, the Internet connection breaks from time to time and the JVM can't provide enough memory and we're left with a daunting StackOverflowError.

If we fail to handle such conditions, the whole application will end up in ruins, and all other code becomes obsolete. Therefore, we must be able to write code that can adapt to such situations.

Imagine a company not being able to resolve a simple issue that arose after ordering a product - you don't want your application to work that way.

Exception Hierarchy

All of this just begs the question - what are these exceptions in the eyes of Java and the JVM?

Exceptions are, after all, simply Java objects that extend the Throwable interface:

                                        ---> Throwable <--- 
                                        |    (checked)     |
                                        |                  |
                                        |                  |
                                ---> Exception           Error
                                |    (checked)        (unchecked)
                                |
                          RuntimeException
                            (unchecked)

When we talk about exceptional conditions, we are usually referring to one of the three:

  • Checked Exceptions
  • Unchecked Exceptions / Runtime Exceptions
  • Errors

Note: The terms "Runtime" and "Unchecked" are often used interchangeably and refer to the same kind of exceptions.

Checked Exceptions

Checked Exceptions are the exceptions that we can typically foresee and plan ahead in our application. These are also exceptions that the Java Compiler requires us to either handle-or-declare when writing code.

The handle-or-declare rule refers to our responsibility to either declare that a method throws an exception up the call stack - without doing much to prevent it or handle the exception with our own code, which typically leads to the recovery of the program from the exceptional condition.

This is the reason why they're called checked exceptions. The compiler can detect them before runtime, and you're aware of their potential existence while writing code.

Unchecked Exceptions

Unchecked Exceptions are the exceptions that typically occur due to human, rather than an environmental error. These exceptions are not checked during compile-time, but at runtime, which is the reason they're also called Runtime Exceptions.

They can often be countered by implementing simple checks before a segment of code that could potentially be used in a way that forms a runtime exception, but more on that later on.

Errors

Errors are the most serious exceptional conditions that you can run into. They are often irrecoverable and there's no real way to handle them. The only thing we, as developers, can do is optimize the code in hopes that the errors never occur.

Errors can occur due to human and environmental errors. Creating an infinitely recurring method can lead to a StackOverflowError, or a memory leak can lead to an OutOfMemoryError.

How to Handle Exceptions

throw and throws

The easiest way to take care of a compiler error when dealing with a checked exception is to simply throw it.

public File getFile(String url) throws FileNotFoundException {
    // some code
    throw new FileNotFoundException();
}

We are required to mark our method signature with a throws clause. A method can add as many exceptions as needed in its throws clause, and can throw them later on in the code, but doesn't have to. This method doesn't require a return statement, even though it defines a return type. This is because it throws an exception by default, which ends the flow of the method abruptly. The return statement, therefore, would be unreachable and cause a compilation error.

Keep in mind that anyone who calls this method also needs to follow the handle-or-declare rule.

When throwing an exception, we can either throw a new exception, like in the preceding example, or a caught exception.

try-catch Blocks

A more common approach would be to use a try-catch block to catch and handle the arising exception:

public String readFirstLine(String url) throws FileNotFoundException {
    try {
        Scanner scanner = new Scanner(new File(url));
        return scanner.nextLine();
    } catch(FileNotFoundException ex) {
        throw ex; 
    }
}

In this example, we "marked" a risky segment of code by encasing it within a try block. This tells the compiler that we're aware of a potential exception and that we're intending to handle it if it arises.

This code tries to read the contents of the file, and if the file is not found, the FileNotFoundException is caught and re-thrown. More on this topic later.

Running this piece of code without a valid URL will result in a thrown exception:

Exception in thread "main" java.io.FileNotFoundException: some_file (The system cannot find the file specified) <-- some_file doesn't exist
    at java.io.FileInputStream.open0(Native Method)
    at java.io.FileInputStream.open(FileInputStream.java:195)
    at java.io.FileInputStream.<init>(FileInputStream.java:138)
    at java.util.Scanner.<init>(Scanner.java:611)
    at Exceptions.ExceptionHandling.readFirstLine(ExceptionHandling.java:15) <-- Exception arises on the the     readFirstLine() method, on line 15
    at Exceptions.ExceptionHandling.main(ExceptionHandling.java:10) <-- readFirstLine() is called by main() on  line 10
...

Alternatively, we can try to recover from this condition instead of re-throwing:

public static String readFirstLine(String url) {
    try {
        Scanner scanner = new Scanner(new File(url));
        return scanner.nextLine();
    } catch(FileNotFoundException ex) {
        System.out.println("File not found.");
        return null;
    }
}

Running this piece of code without a valid URL will result in:

File not found.

finally Blocks

Introducing a new kind of block, the finally block executes regardless of what happens in the try block. Even if it ends abruptly by throwing an exception, the finally block will execute.

This was often used to close the resources that were opened in the try block since an arising exception would skip the code closing them:

public String readFirstLine(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));   
    try {
        return br.readLine();
    } finally {
        if(br != null) br.close();
    }
}

However, this approach has been frowned upon after the release of Java 7, which introduced a better and cleaner way to close resources, and is currently seen as bad practice.

try-with-resources Statement

The previously complex and verbose block can be substituted with:

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

It's much cleaner and it's obviously simplified by including the declaration within the parentheses of the try block.

Additionally, you can include multiple resources in this block, one after another:

static String multipleResources(String path) throws IOException {
    try(BufferedReader br = new BufferedReader(new FileReader(path));
        BufferedWriter writer = new BufferedWriter(path, charset)) {
        // some code
    }
}

This way, you don't have to concern yourself with closing the resources yourself, as the try-with-resources block ensures that the resources will be closed upon the end of the statement.

Multiple catch Blocks

When the code we're writing can throw more than one exception, we can employ several catch blocks to handle them individually:

public void parseFile(String filePath) {
    try {
        // some code 
    } catch (IOException ex) {
        // handle
    } catch (NumberFormatException ex) {
        // handle
    }
}

When the try block incurs an exception, the JVM checks whether the first caught exception is an appropriate one, and if not, goes on until it finds one.

Note: Catching a generic exception will catch all of its subclasses so it's not required to catch them separately.

Catching a FileNotFound exception isn't necessary in this example, because it extends from IOException, but if the need arises, we can catch it before the IOException:

public void parseFile(String filePath) {
    try {
        // some code 
    } catch(FileNotFoundException ex) {
        // handle
    } catch (IOException ex) {
        // handle
    } catch (NumberFormatException ex) {
        // handle
    }
}

This way, we can handle the more specific exception in a different manner than a more generic one.

Note: When catching multiple exceptions, the Java compiler requires us to place the more specific ones before the more general ones, otherwise they would be unreachable and would result in a compiler error.

Union catch Blocks

To reduce boilerplate code, Java 7 also introduced union catch blocks. They allow us to treat multiple exceptions in the same manner and handle their exceptions in a single block:

public void parseFile(String filePath) {
    try {
        // some code 
    } catch (IOException | NumberFormatException ex) {
        // handle
    } 
}

How to throw exceptions

Sometimes, we don't want to handle exceptions. In such cases, we should only concern ourselves with generating them when needed and allowing someone else, calling our method, to handle them appropriately.

Throwing a Checked Exception

When something goes wrong, like the number of users currently connecting to our service exceeding the maximum amount for the server to handle seamlessly, we want to throw an exception to indicate an exceptional situation:

    public void countUsers() throws TooManyUsersException {
       int numberOfUsers = 0;
           while(numberOfUsers < 500) {
               // some code
               numberOfUsers++;
        }
        throw new TooManyUsersException("The number of users exceeds our maximum 
            recommended amount.");
    }
}
Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

This code will increase numberOfUsers until it exceeds the maximum recommended amount, after which it will throw an exception. Since this is a checked exception, we have to add the throws clause in the method signature.

To define an exception like this is as easy as writing the following:

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

Throwing an Unchecked Exception

Throwing runtime exceptions usually boils down to validation of input, since they most often occur due to faulty input - either in the form of an IllegalArgumentException, NumberFormatException, ArrayIndexOutOfBoundsException, or a NullPointerException:

public void authenticateUser(String username) throws UserNotAuthenticatedException {
    if(!isAuthenticated(username)) {
        throw new UserNotAuthenticatedException("User is not authenticated!");
    }
}

Since we're throwing a runtime exception, there's no need to include it in the method signature, like in the example above, but it's often considered good practice to do so, at least for the sake of documentation.

Again, defining a custom runtime exception like this one is as easy as:

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

Re-throwing

Re-throwing an exception was mentioned before so here's a short section to clarify:

public String readFirstLine(String url) throws FileNotFoundException {
    try {
        Scanner scanner = new Scanner(new File(url));
        return scanner.nextLine();
    } catch(FileNotFoundException ex) {
        throw ex; 
    }
}

Re-throwing refers to the process of throwing an already caught exception, rather than throwing a new one.

Wrapping

Wrapping, on the other hand, refers to the process of wrapping an already caught exception, within another exception:

public String readFirstLine(String url) throws FileNotFoundException {
    try {
        Scanner scanner = new Scanner(new File(url));
        return scanner.nextLine();
    } catch(FileNotFoundException ex) {
        throw new SomeOtherException(ex); 
    }
}

Re-throwing Throwable or _Exception*?

These top-level classes can be caught and re-thrown, but how to do so can vary:

public void parseFile(String filePath) {
    try {
        throw new NumberFormatException();
    } catch (Throwable t) {
        throw t;
    }
}

In this case, the method is throwing a NumberFormatException which is a runtime exception. Because of this, we don't have to mark the method signature with either NumberFormatException or Throwable.

However, if we throw a checked exception within the method:

public void parseFile(String filePath) throws Throwable {
    try {
        throw new IOException();
    } catch (Throwable t) {
        throw t;
    }
}

We now have to declare that the method is throwing a Throwable. Why this can be useful is a broad topic that is out of scope for this blog, but there are usages for this specific case.

Exception Inheritance

Subclasses that inherit a method can only throw fewer checked exceptions than their superclass:

public class SomeClass {
   public void doSomething() throws SomeException {
        // some code
    }
}

With this definition, the following method will cause a compiler error:

public class OtherClass extends SomeClass {
    @Override
    public void doSomething() throws OtherException {
        // some code
    }
}

Best and Worst Exception Handling Practices

With all that covered, you should be pretty familiar with how exceptions work and how to use them. Now, let's cover the best and worst practices when it comes to handling exceptions which we hopefully understand fully now.

Best Exception Handling Practices

Avoid Exceptional Conditions

Sometimes, by using simple checks, we can avoid an exception forming altogether:

public Employee getEmployee(int i) {
    Employee[] employeeArray = {new Employee("David"), new Employee("Rhett"), new 
        Employee("Scott")};
    
    if(i >= employeeArray.length) {
        System.out.println("Index is too high!");
        return null;
    } else {
        System.out.println("Employee found: " + employeeArray[i].name);
        return employeeArray[i];
    }
  }
}

Calling this method with a valid index would result in:

Employee found: Scott

But calling this method with an index that's out of bounds would result in:

Index is too high!

In any case, even though the index is too high, the offending line of code will not execute and no exception will arise.

Use try-with-resources

As already mentioned above, it's always better to use the newer, more concise and cleaner approach when working with resources.

Close resources in try-catch-finally

If you're not utilizing the previous advice for any reason, at least make sure to close the resources manually in the finally block.

I won't include a code example for this since both have already been provided, for brevity.

Worst Exception Handling Practices

Swallowing Exceptions

If your intention is to simply satisfy the compiler, you can easily do so by swallowing the exception:

public void parseFile(String filePath) {
    try {
        // some code that forms an exception
    } catch (Exception ex) {}
}

Swallowing an exception refers to the act of catching an exception and not fixing the issue.

This way, the compiler is satisfied since the exception is caught, but all the relevant useful information that we could extract from the exception for debugging is lost, and we didn't do anything to recover from this exceptional condition.

Another very common practice is to simply print out the stack trace of the exception:

public void parseFile(String filePath) {
    try {
        // some code that forms an exception
    } catch(Exception ex) {
        ex.printStackTrace();
    }
}

This approach forms an illusion of handling. Yes, while it is better than simply ignoring the exception, by printing out the relevant information, this doesn't handle the exceptional condition any more than ignoring it does.

Return in a finally Block

According to the JLS (Java Language Specification):

If execution of the try block completes abruptly for any other reason R, then the finally block is executed, and then there is a choice.

So, in the terminology of the documentation, if the finally block completes normally, then the try statement completes abruptly for reason R.

If the finally block completes abruptly for reason S, then the try statement completes abruptly for reason S (and reason R is discarded).

In essence, by abruptly returning from a finally block, the JVM will drop the exception from the try block and all valuable data from it will be lost:

public String doSomething() {
    String name = "David";
    try {
        throw new IOException();
    } finally {
        return name;
    }
}

In this case, even though the try block throws a new IOException, we use return in the finally block, ending it abruptly. This causes the try block to end abruptly due to the return statement, and not the IOException, essentially dropping the exception in the process.

Throwing in a finally Block

Very similar to the previous example, using throw in a finally block will drop the exception from the try-catch block:

public static String doSomething() {
    try {
        // some code that forms an exception
    } catch(IOException io) {
        throw io;
    } finally {
        throw new MyException();
    }
}

In this example, the MyException thrown inside the finally block will overshadow the exception thrown by the catch block and all valuable information will be dropped.

Simulating a goto statement

Critical thinking and creative ways to find a solution to a problem is a good trait, but some solutions, as creative as they are, are ineffective and redundant.

Java doesn't have a goto statement like some other languages but rather uses labels to jump around the code:

public void jumpForward() {
    label: {
        someMethod();
        if (condition) break label;
        otherMethod();
    }
}

Yet still some people use exceptions to simulate them:

public void jumpForward() {
    try {
      // some code 1
      throw new MyException();
      // some code 2
    } catch(MyException ex) {
      // some code 3
    }
}

Using exceptions for this purpose is ineffective and slow. Exceptions are designed for exceptional code and should be used for exceptional code.

Logging and Throwing

When trying to debug a piece of code and finding out what's happening, don't both log and throw the exception:

public static String readFirstLine(String url) throws FileNotFoundException {
    try {
        Scanner scanner = new Scanner(new File(url));
        return scanner.nextLine();
    } catch(FileNotFoundException ex) {
        LOGGER.error("FileNotFoundException: ", ex);
        throw ex;
    }
}

Doing this is redundant and will simply result in a bunch of log messages which aren't really needed. The amount of text will reduce the visibility of the logs.

Catching Exception or Throwable

Why don't we simply catch Exception or Throwable, if it catches all subclasses?

Unless there's a good, specific reason to catch any of these two, it's generally not advised to do so.

Catching Exception will catch both checked and runtime exceptions. Runtime exceptions represent problems that are a direct result of a programming problem, and as such shouldn't be caught since it can't be reasonably expected to recover from them or handle them.

Catching Throwable will catch everything. This includes all errors, which aren't actually meant to be caught in any way.

Conclusion

In this article, we've covered exceptions and exception handling from the ground up. Afterwards, we've covered the best and worst exception handling practices in Java.

Hopefully you found this blog informative and educational, happy coding!

Last Updated: July 24th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

David LandupAuthor

Entrepreneur, Software and Machine Learning Engineer, with a deep fascination towards the application of Computation and Deep Learning in Life Sciences (Bioinformatics, Drug Discovery, Genomics), Neuroscience (Computational Neuroscience), robotics and BCIs.

Great passion for accessible education and promotion of reason, science, humanism, and progress.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms