As a programming newbie, the concept of exception handling can be tough to wrap your head around. Not that the concept itself is difficult, but the terminology can make it seem more advanced than it is. And it's such a powerful feature that it's prone to misuse and abuse.

In this article, you'll learn what exceptions are, why they're important, how to use them, and common mistakes to avoid. Most modern languages have some kind of exception handling, so if you ever move on from Java, you can take most of these tips with you.

Understanding Java Exceptions

In Java, an exception is an object that indicates something abnormal (or "exceptional") occurred during your application's run. Such exceptions are thrown, which basically means an exception object is created (similar to how errors are "raised").

The beauty is that you can catch thrown exceptions, which lets you deal with the abnormal condition and allow your application to continue running as if nothing went wrong. For example, whereas a null pointer in C might crash your application, Java lets you throw and catch

        NullPointerException
    

s before a null variable has a chance to cause a crash.

Remember, an exception is just an object, but with one important characteristic: it must be extended from the

        Exception
    

class or any subclass of

        Exception
    

. While Java has all kinds of built-in exceptions, you can also create your own if you wish. Some of the most common Java exceptions include:

  •         NullPointerException
        
  •         NumberFormatException
        
  •         IllegalArgumentException
        
  •         RuntimeException
        
  •         IllegalStateException
        

So what happens when you throw an exception?

First, Java looks within the immediate method to see if there's code that handles the kind of exception you threw. If a handler doesn't exist, it looks at the method that called the current method to see if a handle exists there. If not, it looks at the method that called that method, and then the next method, etc. If the exception isn't caught, the application prints a stack trace and then crashes. (Actually it's more nuanced than simply crashing, but that's an advanced topic beyond this article's scope.)

A stack trace is a list of all the methods that Java traversed while looking for an exception handler. Here's what a stack trace looks like:

        Exception in thread "main" java.lang.NullPointerException
  at com.example.myproject.Book.getTitle(Book.java:16)
  at com.example.myproject.Author.getBookTitles(Author.java:25)
  at com.example.myproject.Bootstrap.main(Bootstrap.java:14)

We can glean a lot from this. First, the thrown exception was a

        NullPointerException
    

. It occurred in the

        getTitle()
    

method on line 16 of Book.java. That method was called from

        getBookTitles()
    

on line 25 of Author.java. That method was called from

        main()
    

on line 14 of Bootstrap.java. As you can see, knowing all of this makes debugging easier.

But again, the true benefit of exceptions is that you can "handle" the abnormal condition by catching the exception, setting things right, and resuming the application without crashing.

Using Java Exceptions in Code

Let's say you have

        someMethod()
    

that takes an integer and executes some logic that could break if the integer is less than 0 or greater than 100. This could be a good place to throw an exception:

        public void someMethod(int value) {
 if (value < 0 || value > 100) {
 throw new <pre><code class="language-bash">IllegalArgumentException

In order to catch this exception, you need to go to where

        someMethod()
    

is called and use the try-catch block:

        public void callingMethod() {
  try {
    someMethod(200);
    someOtherMethod();
  } catch (IllegalArgumentException e) {
    // handle the exception in here
  }
  // ...
}

Everything within the try block will execute in order until an exception is thrown. As soon as an exception is thrown, all subsequent statements are skipped and the application logic immediately jumps to the catch block.

In our example, we enter the try block and immediately call

        someMethod()
    

. Since 200 isn't between 0 and 100, an

        IllegalArgumentException
    

is thrown. This immediately ends execution of

        someMethod()
    

, skips the rest of the logic in the try block (

        someOtherMethod()
    

is never called), and resumes execution within the catch block.

What would happen if we called

        someMethod(50)
    

instead? The

        IllegalArgumentException
    

would never be thrown.

        someMethod()
    

would execute as normal. The try block would execute as normal, calling

        someOtherMethod()
    

when someMethod() completes. When

        someOtherMethod()
    

ends, the catch block would be skipped and

        callingMethod()
    

would continue.

Note that you can have multiple catch blocks per try block:

        public void callingMethod() {
  try {
    someMethod(200);
    someOtherMethod();
  } catch (IllegalArgumentException e) {
    // handle the exception in here
  } catch (NullPointerException e) {
    // handle the exception in here
  }
  // ...
}

Also note that an optional finally block exists as well:

        public void method() {
  try {
    // ...
  } catch (Exception e) {
    // ...
  } finally {
    // ...
  }
}

The code within a finally block is always executed no matter what. If you have a return statement in the try block, the finally block is executed before returning out of the method. If you throw another exception in the catch block, the finally block is executed before the exception is thrown.

You should use the finally block when you have objects that need to be cleaned up before the method ends. For example, if you opened a file in the try block and later threw an exception, the finally block lets you close the file before leaving the method.

Note that you can have a finally block without a catch block:

        public void method() {
  try {
    // ...
  } finally {
    // ...
  }
}

This lets you do any necessary cleanup while allowing thrown exceptions to propagate up the method invocation stack (i.e. you don't want to handle the exception here but you still need to clean up first).

Checked vs. Unchecked Exceptions in Java

Unlike most languages, Java distinguishes between checked exceptions and unchecked exceptions (e.g. C# only has unchecked exceptions). A checked exception must be caught in the method where the exception is thrown or else the code won't compile.

To create a checked exception, extend from

        Exception
    

. To create an unchecked exception, extend from

        RuntimeException
    

.

Any method that throws a checked exception must denote this in the method signature using the throws keyword. Since Java's built-in

        IOException
    

is a checked exception, the following code won't compile:

        public void wontCompile() {
  // ...
  if (someCondition) {
    throw new IOException();
  }
  // ...
}

You must first declare that it throws a checked exception:

        public void willCompile() throws IOException {
  // ...
  if (someCondition) {
    throw new IOException();
  }
  // ...
}

Note that a method can be declared as throwing an exception but never actually throw an exception. Even so, the exception will still need to be caught or else the code won't compile.

When should you use checked or unchecked exceptions?

The official Java documentation has a page on this question. It sums up the difference with a succinct rule of thumb: "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."

But this guideline may be outdated. On the one hand, checked exceptions do result in more robust code. On the other hand, no other language has checked exceptions in the same manner as Java, which shows two things: one, the feature isn't useful enough for other languages to steal it, and two, you can absolutely live without them. Plus, checked exceptions don't play nicely with lambda expressions introduced in Java 8.

Guidelines for Java Exceptions Usage

Exceptions are useful but easily misused and abused. Here are a few tips and best practices to help you avoid making a mess of them.

  • Prefer specific exceptions to general exceptions. Use
            NumberFormatException
        
    over
            IllegalArgumentException
        
    when possible, otherwise use
            IllegalArgumentException
        
    over
            RuntimeException
        
    when possible.
  • Never catch
            Throwable
        
    !
    The
            Exception
        
    class actually extends
            Throwable
        
    , and the catch block actually works with
            Throwable
        
    or any class that extends Throwable. However, the
            Error
        
    class also extends
            Throwable
        
    , and you never want to catch an
            Error
        
    because
            Error
        
    s indicate serious unrecoverable issues.
  • Never catch
            Exception
        
    !
            InterruptedException
        
    extends
            Exception
        
    , so any block that catches
            Exception
        
    will also catch
            InterruptedException
        
    , and that's a very important exception that you don't want to mess with (especially in multi-threaded applications) unless you know what you're doing. If you don't know which exception to catch instead, consider not catching anything.
  • Use descriptive messages to ease debugging. When you throw an exception, you can provide a
            String
        
    message as an argument. This message can be accessed in the catch block using the
            Exception.getMessage()
        
    method, but if the exception is never caught, the message will also appear as part of the stack trace.
  • Try not to catch and ignore exceptions. To get around the inconvenience of checked exceptions, a lot of newbie and lazy programmers will set up a catch block but leave it empty. Bad! Always handle it gracefully, but if you can't, at the very least print out a stack trace so you know the exception was thrown. You can do this using the
            Exception.printStackTrace()
        
    method.
  • Beware of overusing exceptions. When you have a hammer, everything looks like a nail. When you first learn about exceptions, you may feel obliged to turn everything into an exception... to the point where most of your application's control flow comes down to exception handling. Remember, exceptions are meant for "exceptional" occurrences!

Now you should be comfortable enough with exceptions to understand what they are, why they're used, and how to incorporate them into your own code. If you don't fully understand the concept, that's okay! It took me a while for it to "click" in my head, so don't feel like you need to rush it. Take your time.

Got any questions? Know of any other exception-related tips that I missed? Share them in the comments below!