Skip to content
elephantoo

Exception handling

Lesson 21 of 43 16 min read

try/catch/finally, the exception hierarchy, checked vs unchecked, throw, throws and multi-catch.


Things go wrong: files go missing, users type letters where numbers belong, networks drop, and bugs lurk. Java handles errors with exceptions: objects that describe a problem and interrupt normal flow until some code handles them. Good exception handling is the difference between a program that crashes with a stack trace and one that explains the problem and recovers.

What happens without handling#

Crash.java
public class Crash {
    public static void main(String[] args) {
        int[] data = {1, 2, 3};
        System.out.println("Before");
        System.out.println(data[5]);
        System.out.println("After");   // never reached
    }
}
Output
Before
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3
	at Crash.main(Crash.java:5)

The exception was thrown, nothing caught it, so the JVM printed the stack trace and ended the program.

Reading a stack trace

  • The first line gives the exception type and message: here ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3.
  • Each at line is a method call, most recent first, with the file and line number.
  • Find the first line that points to your code. That is where to start debugging.
  • A Caused by: section shows the underlying exception when one exception wraps another.

try / catch#

Put risky code in a try block and handle failures in catch:

ParseAge.java
public class ParseAge {
    public static void main(String[] args) {
        String[] inputs = {"42", "forty-two", "-7"};
        for (String input : inputs) {
            try {
                int age = Integer.parseInt(input);
                System.out.println("Parsed: " + age);
            } catch (NumberFormatException e) {
                System.out.println("Not a number: " + e.getMessage());
            }
        }
        System.out.println("Program continues");
    }
}
Output
Parsed: 42
Not a number: For input string: "forty-two"
Parsed: -7
Program continues

When parseInt throws, the rest of the try block is skipped and control jumps to the matching catch. After the catch, execution continues normally.

Useful methods on every exception: getMessage(), getClass().getSimpleName(), getCause() and printStackTrace().

The exception hierarchy#

Output
Throwable
├── Error                      serious JVM problems: don't catch
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception                  CHECKED (except RuntimeException)
    ├── IOException
    │   └── FileNotFoundException
    ├── SQLException
    ├── InterruptedException
    └── RuntimeException       UNCHECKED
        ├── NullPointerException
        ├── IllegalArgumentException
        │   └── NumberFormatException
        ├── IllegalStateException
        ├── ArithmeticException
        ├── IndexOutOfBoundsException
        │   └── ArrayIndexOutOfBoundsException
        ├── ClassCastException
        └── UnsupportedOperationException

A catch for a type also catches all its subclasses: catch (IllegalArgumentException e) catches NumberFormatException too.

Checked vs unchecked exceptions#

This distinction is unique to Java and very important:

CheckedUnchecked
ExtendsException (not RuntimeException)RuntimeException or Error
Compilerforces you to catch or declare itno requirement
Typical meaningan external condition a correct program should anticipatea programming bug or invalid argument
ExamplesIOException, SQLException, InterruptedExceptionNullPointerException, IllegalArgumentException

If you call a method that throws a checked exception and neither catch nor declare it, the code does not compile:

Output
error: unreported exception IOException; must be caught or declared to be thrown

throws: declaring exceptions#

A method that does not want to handle a checked exception itself declares it with throws, passing responsibility to its caller:

ReadConfig.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class ReadConfig {
    static String loadConfig(String file) throws IOException {   // caller must deal with it
        return Files.readString(Path.of(file));
    }

    public static void main(String[] args) {
        try {
            String text = loadConfig("missing-config.properties");
            System.out.println(text);
        } catch (IOException e) {
            System.out.println("Could not read config: " + e.getClass().getSimpleName());
        }
    }
}
Output
Could not read config: NoSuchFileException

NoSuchFileException is a subclass of IOException, so the catch handles it.

throw: raising your own exceptions#

Use throw to signal that something is wrong. This is called failing fast: detect the problem as early as possible, with a clear message:

Withdraw.java
public class Withdraw {
    static double balance = 1000;

    static void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive, got " + amount);
        }
        if (amount > balance) {
            throw new IllegalStateException("Insufficient funds: balance " + balance + ", requested " + amount);
        }
        balance -= amount;
    }

    public static void main(String[] args) {
        double[] attempts = {300, -5, 5000};
        for (double a : attempts) {
            try {
                withdraw(a);
                System.out.println("Withdrew " + a + ", balance " + balance);
            } catch (IllegalArgumentException | IllegalStateException e) {
                System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
            }
        }
    }
}
Output
Withdrew 300.0, balance 700.0
IllegalArgumentException: Amount must be positive, got -5.0
IllegalStateException: Insufficient funds: balance 700.0, requested 5000.0

Don't confuse the keywords: throw (a statement) raises an exception now; throws (in a method signature) declares that the method might.

Good standard exceptions to reuse:

ExceptionUse when
IllegalArgumentExceptionan argument is invalid
IllegalStateExceptionthe object is in the wrong state for this call
NullPointerExceptiona required argument is null (Objects.requireNonNull(x, "x"))
UnsupportedOperationExceptionthe operation is not supported
IndexOutOfBoundsExceptionan index is out of range

Multiple catch blocks and multi-catch#

Order catch blocks from most specific to most general:

MultiCatch.java
public class MultiCatch {
    static void risky(int choice) throws Exception {
        switch (choice) {
            case 1 -> throw new NumberFormatException("bad number");
            case 2 -> throw new ArithmeticException("divide by zero");
            case 3 -> throw new Exception("something general");
            default -> System.out.println("all good");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i <= 3; i++) {
            try {
                risky(i);
            } catch (NumberFormatException e) {
                System.out.println("Specific: " + e.getMessage());
            } catch (ArithmeticException | IllegalStateException e) {   // multi-catch
                System.out.println("Maths or state: " + e.getMessage());
            } catch (Exception e) {
                System.out.println("General: " + e.getMessage());
            }
        }
    }
}
Output
all good
Specific: bad number
Maths or state: divide by zero
General: something general
  • Only the first matching catch runs.
  • Multi-catch (A | B) shares one handler; the types must not be subclasses of each other.

finally: always clean up#

Code in finally runs whether the try succeeds, throws, or even returns:

FinallyDemo.java
public class FinallyDemo {
    static int divide(int a, int b) {
        try {
            System.out.println("Dividing " + a + " by " + b);
            return a / b;
        } catch (ArithmeticException e) {
            System.out.println("Cannot divide by zero");
            return 0;
        } finally {
            System.out.println("finally runs");
        }
    }

    public static void main(String[] args) {
        System.out.println("Result: " + divide(10, 2));
        System.out.println("Result: " + divide(1, 0));
    }
}
Output
Dividing 10 by 2
finally runs
Result: 5
Dividing 1 by 0
Cannot divide by zero
finally runs
Result: 0

finally is for releasing resources: closing files, database connections, locks. For anything AutoCloseable, try-with-resources (next lesson) does this more safely. Never return from inside finally: it silently discards any exception in flight.

How exceptions propagate#

An uncaught exception travels up the call stack until some method catches it:

Propagation.java
public class Propagation {
    static void level3() { throw new IllegalStateException("deep problem"); }
    static void level2() { level3(); }
    static void level1() {
        try {
            level2();
        } catch (IllegalStateException e) {
            System.out.println("Caught in level1: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        level1();
        System.out.println("main finished normally");
    }
}
Output
Caught in level1: deep problem
main finished normally

Catch an exception at the level that can actually do something useful about it: retry, use a default, show a message, or translate it. If a method can't handle it meaningfully, let it propagate.

Best practices#

  • Never swallow exceptions silently. catch (Exception e) { } hides bugs. At minimum, log it.
  • Catch specific types, not Exception or Throwable, except at the very top of an application (e.g. to log and show a friendly error).
  • Don't use exceptions for normal control flow. Check hasNextInt() or map.containsKey() when absence is expected.
  • Don't catch Errors like OutOfMemoryError; the JVM is in trouble and you can rarely recover.
  • Write helpful messages that include the offending value: "Invalid quantity: -3" beats "error".
  • Validate early with Objects.requireNonNull and argument checks at the start of public methods.
  • Preserve the cause when wrapping exceptions (next lesson).

Common mistakes#

  • Catching a general type before a specific one (compile error: the exception has already been caught).
  • Printing e and continuing as if nothing happened, leaving the program in a broken state.
  • Declaring throws Exception everywhere, which forces every caller to handle "anything".
  • Assuming finally won't run when the try block returns.

What's next#

Standard exceptions only go so far. Next you will design custom exceptions for your own domain, chain causes, and use try-with-resources to close files and connections automatically.

Check your understanding

Quick quiz

0/3 answered
  1. 1.Which of these is a checked exception?

  2. 2.When does a finally block run?

  3. 3.Why does catch (Exception e) { } catch (IOException e) { } fail to compile?

Finished reading?

Mark this lesson complete to track your progress.