Skip to content

19. Exceptions and Error Handling

Table of Contents


Exceptions are Java’s structured mechanism for handling abnormal conditions at runtime.

They allow programs to separate normal execution flow from error-handling logic, improving robustness, readability, and correctness.

19.1 Exception hierarchy and types

All exceptions derive from Throwable.

The hierarchy defines which conditions are recoverable, which must be declared, and which represent fatal system failures.

java.lang.Object
└── java.lang.Throwable
    ├── java.lang.Error
    └── java.lang.Exception
        └── java.lang.RuntimeException

19.1.1 Throwable

  • Base class for all errors and exceptions
  • Supports message, cause, and stack trace
  • Only Throwable (and subclasses) can be thrown or caught

19.1.2 Error (unchecked)

  • Represents serious JVM or system problems
  • Not intended to be caught or handled
  • Examples: OutOfMemoryError, StackOverflowError

Note

Errors indicate conditions from which the application is generally not expected to recover.

19.1.3 Checked Exceptions (Exception)

  • Subclasses of Exception excluding RuntimeException
  • Represent conditions that applications may want to handle
  • Must be either caught or declared
  • Examples: IOException, SQLException

19.1.4 Unchecked Exceptions (RuntimeException)

  • Subclasses of RuntimeException
  • Not required to be declared or caught
  • Usually represent programming errors
  • Examples: NullPointerException, IllegalArgumentException

19.2 Declaring and throwing exceptions

19.2.1 Declaring exceptions with throws

A method declares checked exceptions using the throws clause. This is part of the method’s API contract.

void readFile(Path p) throws IOException {
    Files.readString(p);
}

Note

  • Only checked exceptions must be declared.
  • Unchecked exceptions may be declared, but are usually omitted.

19.2.2 Throwing exceptions

Exceptions are created with new and thrown explicitly using throw.

if (value < 0) {
    throw new IllegalArgumentException("value must be >= 0");
}
  • throw throws exactly one exception instance
  • throws declares possible exceptions in the method signature

19.3 Overriding methods and exception rules

When overriding a method, exception rules are strictly enforced.

  • An overriding method may throw fewer or narrower checked exceptions
  • It may throw any unchecked exceptions
  • It may throw no new or broader checked exceptions
class Parent {
    void work() throws IOException {}
}

class Child extends Parent {
    @Override
    void work() throws FileNotFoundException {} // OK (subclass)
}

Note

Changing only the unchecked exceptions never breaks the override contract.

Important

Remember: constructors follow a different rule.

A Constructor must declare all the checked exceptions declared in the base constructor (or the superclasses of those checked exceptions).

It may also declare additional checked exceptions. This behavior is the opposite of method overriding.

An overriding method cannot throw any checked exception other than those declared by the overridden method. It may only throw subclasses of those exceptions.


19.4 Handling exceptions: try, catch, finally

19.4.1 Basic try-catch syntax

try {
    riskyOperation();
} catch (IOException e) {
    handle(e);
}
  • A try block must be followed by at least one catch or a finally
  • Catches are checked top-down

19.4.2 Multiple catch blocks

Multiple catch blocks allow different handling for different exception types.

try {
    process();
} catch (FileNotFoundException e) {
    recover();
} catch (IOException e) {
    log();
}

Note

More specific exceptions must come before more general ones, otherwise compilation fails. If you place a catch for a superclass (e.g. IOException) before a catch for a subclass (e.g. FileNotFoundException), the subclass catch becomes unreachable.

19.4.3 Multi-catch (Java 7+)

try {
    process();
} catch (IOException | SQLException e) {
    log(e);
}
  • Exception types must be unrelated (no parent/child)
  • The caught variable is implicitly final

19.4.4 finally block

The finally block executes regardless of whether an exception is thrown, except in extreme JVM termination cases.

try {
    open();
} finally {
    close();
}
  • Used for cleanup logic
  • Executes even if return is used in try and/or catch block

Note

A finally block can override a return value or swallow an exception. This is generally discouraged because it makes the control flow harder to reason about.

Important

When both a catch block and a finally block throw exceptions, the exception thrown in the finally block is the one that is propagated from the method.

The exception thrown in the catch block is lost and is not added to the suppressed exceptions list.

try {
    throw new RuntimeException("try");
} catch (RuntimeException e) {
    throw new RuntimeException("catch");
} finally {
    throw new RuntimeException("finally");
}

In this case, only the "finally" exception is thrown.


19.5 Automatic Resource Management (try-with-resources)

Try-with-resources provides automatic closing of resources that implement AutoCloseable.

It eliminates the need for explicit finally cleanup in most cases.

19.5.1 Basic syntax

try (BufferedReader br = Files.newBufferedReader(path)) {
    return br.readLine();
}
  • Resources are closed automatically
  • Closure happens even if an exception is thrown
  • Resources are closed before any catch or finally block executes.
try (Resource a = new Resource()) {
    a.read();
} finally {
    a.close();  // ❌ Compile-time error: a is out of scope here
}

19.5.2 Declaring multiple resources

try (InputStream in = Files.newInputStream(p);
        OutputStream out = Files.newOutputStream(q)) {
    in.transferTo(out);
}
  • Resources are closed in reverse order of declaration

19.5.3 Scope of resources

  • Resources are in scope only inside the try block
  • They are implicitly final
  • Since Java 9, you can declare resources ahead of time, outside the try-with-resources, provided they are declared as final or are effectively final.
final var firstWriter = Files.newBufferedWriter(filePath);

try (firstWriter; var secondWriter = Files.newBufferedWriter(filePath)) {
    // CODE
}

Note

Attempting to reassign a resource variable causes a compilation error.

Resource a = new Resource();
try(a){ // since Java 9
  ...
}finally{
   a.close(); // this code will compile but the resource referred to by the reference 'a', has been closed.
}

19.6 Suppressed exceptions

When both the try block and the resource’s close() method throw exceptions, Java preserves the primary exception and suppresses the others.

try (BadResource r = new BadResource()) {
    throw new RuntimeException("main");
}

If close() also throws an exception, it becomes suppressed.

catch (Exception e) {
    for (Throwable t : e.getSuppressed()) {
        System.out.println(t);
    }
}
  • Primary exception is thrown
  • Secondary exceptions are accessible via getSuppressed()

Important

Suppressed exceptions are generated only by the implicit finally block created by try-with-resources.

In contrast, exceptions thrown in an explicit finally block are not suppressed: they replace any previous exception and become the only exception propagated.


19.7 Exceptions summary

  • Checked exceptions must be caught or declared
  • Overriding methods may not widen checked exceptions
  • Use multi-catch for shared handling logic
  • Prefer try-with-resources over finally cleanup
  • Resources close in reverse order
  • Suppressed exceptions preserve full failure context