Skip to content

Handling Exceptions

Step 7 of 20

Handling Exceptions

Introduction

This lesson covers the fourth objective of the Oracle Certified Professional Java SE 21 Developer exam (1Z0-830): Handling Exceptions. Oracle does not publish a weighting per objective, but exception handling is a rich source of "does it compile?" and "what is printed?" questions because the rules are precise: which exceptions must be declared, which catch orders are legal, what finally does to a returned value, and how try-with-resources closes and suppresses.

You will learn the exception hierarchy, the full try/catch/finally semantics, multi-catch, try-with-resources with suppressed exceptions, the overriding rules for throws, and how to design custom exceptions. Every section ends with the traps the exam uses.

Learning objectives

After this lesson you will be able to:

  • Classify any exception as checked, unchecked or an error, and say what the compiler demands for each.
  • Trace try/catch/finally flow, including return inside finally and exceptions thrown from catch blocks.
  • Write legal multi-catch clauses and order catch blocks without compile errors.
  • Use try-with-resources, predict the closing order and read suppressed exceptions.
  • Apply the throws rules when overriding methods and implementing interfaces.
  • Create custom checked and unchecked exceptions, with chaining of the cause.
  • Choose between throw, throws, rethrowing and wrapping in realistic code.

1. The exception hierarchy

All throwable types descend from java.lang.Throwable. It has two branches:

  • Error: serious problems the application is not expected to handle (OutOfMemoryError, StackOverflowError, AssertionError). Unchecked.
  • Exception: conditions an application may handle. Under it, RuntimeException and its subclasses are unchecked; every other Exception subclass is checked.
Category Examples Compiler requires handle or declare?
Checked IOException, SQLException, InterruptedException, ClassNotFoundException, Exception itself Yes
Unchecked (runtime) NullPointerException, IllegalArgumentException, ArithmeticException, ClassCastException, ArrayIndexOutOfBoundsException, NumberFormatException, IllegalStateException, UnsupportedOperationException No
Error OutOfMemoryError, StackOverflowError, ExceptionInInitializerError No

"Handle or declare" means: either catch the checked exception in a try/catch, or list it in the throws clause of the method.

static String readFirstLine(Path p) throws IOException {   // declared
    return Files.readAllLines(p).get(0);                   // may throw IOException
}

Remember some specifics: NumberFormatException extends IllegalArgumentException; ArrayIndexOutOfBoundsException and StringIndexOutOfBoundsException extend IndexOutOfBoundsException; FileNotFoundException extends IOException; UncheckedIOException wraps an IOException as a runtime exception.

2. try, catch and finally

try {
    int qty = Integer.parseInt(input);       // may throw NumberFormatException
    process(qty);
} catch (NumberFormatException e) {
    log.warn("bad quantity: " + input);
} catch (IllegalStateException e) {
    ...
} finally {
    metrics.increment("order.parsed");        // always runs
}

Rules:

  • A try needs at least one catch or a finally (or, with resources, neither).
  • Catch blocks are tested top to bottom; the first compatible one runs. A catch for a subclass after a catch for its superclass is a compile error ("exception has already been caught"). Put the most specific first.
  • Catching a checked exception that the try block cannot throw is a compile error (catch (IOException e) around code that never throws it). This does not apply to Exception, Throwable or unchecked types.
  • finally runs after the try and catch blocks whether an exception occurred or not. It does not run only if the JVM exits (System.exit) or the thread is killed or the JVM crashes.
  • An exception thrown from a catch block propagates to the caller after finally runs.

return and finally

A return inside try evaluates its expression first, then runs finally. If finally itself returns or throws, it replaces the earlier result or exception.

static int f() {
    int x = 1;
    try {
        return x;          // value 1 is saved
    } finally {
        x = 2;             // changes the variable, not the saved value
    }
}                          // returns 1

static int g() {
    try {
        throw new RuntimeException("lost");
    } finally {
        return 5;          // swallows the exception, returns 5
    }
}

For a mutable returned object, changes made in finally are visible because the reference is the same; for primitives and immutable values they are not.

3. Multi-catch and rethrow

A multi-catch lists alternatives separated by |:

try {
    run();
} catch (IOException | SQLException e) {
    log.error("infrastructure failure", e);
    throw e;                                  // precise rethrow, e is effectively final
}

Rules:

  • The alternatives must not be related by subclassing: catch (FileNotFoundException | IOException e) is a compile error because one is a subtype of the other.
  • The multi-catch parameter is implicitly final: you cannot assign to e.
  • Its static type is the closest common supertype, so you can call only methods of that supertype.
  • Precise rethrow: when you rethrow an effectively final catch parameter, the compiler knows the real exceptions the try can throw. A method catching Exception e and rethrowing it only needs to declare the checked exceptions thrown inside the try.

4. try-with-resources

Any object implementing AutoCloseable (or Closeable) can be declared in the resource header. It is closed automatically, in reverse order of declaration, before any catch or finally runs.

try (var in = Files.newBufferedReader(src);
     var out = Files.newBufferedWriter(dst)) {
    String line;
    while ((line = in.readLine()) != null) {
        out.write(line);
        out.newLine();
    }
} catch (IOException e) {
    log.error("copy failed", e);
} finally {
    log.info("copy attempted");
}

Details the exam checks:

  • Resources are implicitly final. Since Java 9 you may use an existing variable that is final or effectively final: try (reader) { ... }.
  • close() of AutoCloseable declares throws Exception, so if the resource type is AutoCloseable you must handle Exception; Closeable.close() declares IOException; many classes narrow it further.
  • If the body throws an exception and close() also throws, the body exception is the primary one and the close exception is attached as suppressed; read it with e.getSuppressed().
  • If a resource constructor throws, resources already opened are closed, and the one that failed is not.
  • The order of output for a closing trace is: body, resources closed in reverse, then catch, then finally.
class Res implements AutoCloseable {
    final String name;
    Res(String name) { this.name = name; System.out.print("open " + name + "; "); }
    public void close() { System.out.print("close " + name + "; "); }
}
try (Res a = new Res("A"); Res b = new Res("B")) {
    System.out.print("body; ");
} finally {
    System.out.print("finally");
}
// open A; open B; body; close B; close A; finally

5. Checked exceptions, throw and throws

throw is a statement that raises one exception object; throws is a clause in the method signature that lists exceptions the method may propagate.

  • You can throw a checked exception only if you catch it or declare it.
  • Unreachable code after throw is a compile error.
  • throw null; compiles and produces a NullPointerException at run time.
  • A method may declare unchecked exceptions in throws, but callers need not handle them.

Overriding and throws

An overriding method can declare fewer, narrower or no checked exceptions, but not new or broader checked exceptions. Unchecked exceptions are unrestricted.

class Repo { void save() throws IOException { } }
class FileRepo extends Repo {
    @Override void save() throws FileNotFoundException { }   // narrower: OK
}
class BadRepo extends Repo {
    @Override void save() throws Exception { }               // broader: compile error
}

The same rule applies when implementing an interface method. Constructors do not override, but an implicit super() call must be compatible with what the subclass constructor declares: if the parent constructor throws a checked exception, the child constructor must declare it (or a supertype).

6. Custom exceptions and chaining

Extend Exception for a recoverable condition the caller should be forced to consider, RuntimeException for programming errors or conditions the caller cannot reasonably recover from. Provide constructors for a message and for a cause, so stack traces keep the root problem.

public class EnrollmentException extends Exception {
    private final String courseId;
    public EnrollmentException(String courseId, String message, Throwable cause) {
        super(message, cause);
        this.courseId = courseId;
    }
    public String courseId() { return courseId; }
}

void enroll(String user, String course) throws EnrollmentException {
    try {
        repo.insert(user, course);
    } catch (SQLException e) {
        throw new EnrollmentException(course, "cannot enroll " + user, e);   // wrap, keep cause
    }
}

getCause() returns the wrapped exception, printStackTrace() shows "Caused by:" sections, and initCause can set it later. Do not swallow exceptions with an empty catch; at least log them or rethrow.

assert is related but different: assert cond : message; throws AssertionError only if assertions are enabled with -ea; they are disabled by default, so assertions must never carry side effects needed by the program.

7. Frequent runtime exceptions and their causes

Exception Typical cause
NullPointerException Dereferencing null, unboxing a null Integer
ArrayIndexOutOfBoundsException Index < 0 or >= length
ClassCastException Invalid downcast
ArithmeticException Integer division by zero (floating point gives Infinity/NaN)
NumberFormatException Integer.parseInt("12a")
IllegalArgumentException Argument rejected by a method
IllegalStateException Method called at the wrong time
ConcurrentModificationException Structural change of a list while iterating
UnsupportedOperationException Mutating List.of(...)
ArrayStoreException Storing a wrong type in a covariant array
ExceptionInInitializerError An exception escaping a static initializer
StackOverflowError Unbounded recursion

8. Designing exception boundaries

Good exception handling is as much design as syntax. Three habits make code easier to maintain and also help you reason about exam snippets.

Catch where you can act. A low-level method that cannot recover should let the exception propagate (or wrap it) rather than log and continue. A method near the user interface, such as a web controller, is the right place to turn an exception into an HTTP 400 or 500 response and a log entry.

Do not use exceptions for normal control flow. Parsing a number with try { Integer.parseInt(s) } catch (NumberFormatException e) is acceptable for input validation, but looping until an ArrayIndexOutOfBoundsException occurs is not: it is slow and obscures intent.

Preserve information. When you wrap, always pass the cause. When you rethrow, keep the original object (throw e;) so the stack trace points at the real failure. Creating a new exception with only the message loses the trace. When a cleanup step in finally can itself fail, prefer try-with-resources, which keeps both failures through suppression instead of letting the second hide the first.

Here is a short comparison of the choices you meet on the exam:

Situation Best choice Why
Caller can recover (retry, ask the user) Checked custom exception The compiler forces the caller to decide
Caller made a programming mistake (null, bad index) Unchecked (IllegalArgumentException, NullPointerException) Fixing the code is the remedy, not catching
Resource must be released try-with-resources Closes in reverse order, keeps suppressed errors
Several unrelated failures handled identically Multi-catch One handler, no duplicated code
Lower layer exception should not leak Wrap with cause Keeps layers independent, keeps the root cause

Short example. A payment gateway client throws java.net.http.HttpTimeoutException (a subclass of IOException). The checkout service must show "try again" to the shopper but must never charge twice. Catching IOException at the checkout boundary, logging the order id and returning a retry response is correct; swallowing the exception and returning success would be a business bug that no compiler can detect.

Worked example: order of output

Statement. What is printed?

public class Main {
    static String run() {
        StringBuilder sb = new StringBuilder("a");
        try {
            sb.append("b");
            Object o = "text";
            Integer i = (Integer) o;
            sb.append("c");
        } catch (ClassCastException e) {
            sb.append("d");
            return sb.append("e").toString();
        } finally {
            sb.append("f");
            System.out.print(sb + " ");
        }
        return sb.toString();
    }
    public static void main(String[] args) {
        System.out.println(run());
    }
}

Step 1. sb starts as a; append("b") gives ab.

Step 2. The cast of a String to Integer throws ClassCastException, so append("c") is skipped.

Step 3. The catch block appends d, then e, producing abde. toString() creates the return value "abde", a separate immutable String that is saved.

Step 4. finally appends f to the builder (now abdef) and prints abdef with a trailing space. It cannot change the saved String.

Step 5. The method returns the saved "abde", and main prints it.

Solution. The output is abdef abde.

Why other answers are worse. abdef abdef assumes the builder is returned instead of an already created String. abde abdef reverses the order of finally and the print in main. abc... forgets that the cast fails before c is appended.

Common pitfalls and exam tips

  • Exam tip: first decide compile or not (checked exceptions, catch order, unreachable code), and only then trace the output.
  • A catch of a checked exception that the try body cannot throw is a compile error, but catch (Exception e) is always allowed.
  • Do not return from finally: it hides exceptions and values. Expect questions that depend on it.
  • catch (Exception | RuntimeException e) fails: the types are related.
  • In try-with-resources the resource variable is final; reassigning it in the body is a compile error.
  • If both the body and close() throw, the caller sees the body exception and finds the other in getSuppressed().
  • InterruptedException is checked and thrown by Thread.sleep; forgetting to handle it is a classic compile error.
  • A static initializer throwing an unchecked exception produces ExceptionInInitializerError, and later uses of the class give NoClassDefFoundError.
  • Do not catch Throwable or Error as routine practice.

Mini case study: importing a course file

An admin import job reads a JSON file, parses lessons and inserts them in a database. Failures must be reported with the lesson id, the file must always be closed, and the job must continue with the next file after a parsing error but stop on a database outage.

Solution. Define LessonParseException extends Exception (recoverable per file) and let SQLException propagate as a wrapped IllegalStateException("database unavailable", e) (not recoverable here). Open the file with try-with-resources so it is closed even on failure. Inside the loop over files, catch only LessonParseException, log it and continue. Because IllegalStateException is unchecked and not caught, it ends the job.

for (Path file : files) {
    try (var reader = Files.newBufferedReader(file)) {
        importer.load(reader);                      // throws LessonParseException, SQLException
    } catch (LessonParseException e) {
        log.warn("skipping " + file + " lesson " + e.lessonId(), e);
    } catch (SQLException e) {
        throw new IllegalStateException("database unavailable", e);
    } catch (IOException e) {
        log.warn("cannot read " + file, e);
    }
}

Notice that FileNotFoundException would be caught by the IOException clause, which must appear after the more specific ones; the Exception supertype is never used, so unexpected runtime bugs are not hidden. The wrapping keeps the SQL root cause for diagnosis.

Key terms

  • Checked exception: an Exception that is not a RuntimeException; must be handled or declared.
  • Unchecked exception: a RuntimeException or Error; no compiler requirement.
  • Multi-catch: a single catch clause handling several unrelated exception types.
  • Suppressed exception: an exception from close() attached to the primary exception.
  • Precise rethrow: rethrowing an effectively final catch parameter with its actual thrown types.
  • Exception chaining: wrapping a cause inside a new exception.
  • AutoCloseable: interface whose close() is called by try-with-resources.
  • Handle or declare: the compiler rule for checked exceptions.

Summary checklist

  • I can classify an exception as checked, unchecked or error from the hierarchy.
  • I can order catch blocks correctly and spot "already been caught" errors.
  • I can predict what finally does to returned values and exceptions.
  • I can write multi-catch clauses and know the parameter is final.
  • I can trace resource closing order and read suppressed exceptions.
  • I can apply the throws rules for overriding and constructors.
  • I can design a custom exception that keeps its cause.

Official sources