SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Implementing Exception Handling in Java Applications

Java Exception Handling: try/catch/finally, try-with-resources and Custom Exceptions (1Z0-831)

23 min read1Z0-831 · Implementing Exception Handling in Java ApplicationsUpdated

Java exception handling is the set of language rules for throwing, catching, cleaning up after and declaring exceptions: try/catch/finally, try-with-resources, multi-catch and your own exception classes, which Oracle 1Z0-831 objective 4.1 asks you to use correctly. This lesson covers the checked/unchecked hierarchy and the handle-or-declare rule, how to write custom exceptions with messages and causes, the exact order try, catch and finally run in, the compile-time rules for catch and multi-catch clauses, precise rethrow, how finally interacts with return values, how exceptions propagate, and how try-with-resources closes resources and suppresses secondary failures. Every code sample was compiled and run on JDK 25.

What you’ll learn
  • Classify any exception type as checked or unchecked, including custom exceptions, Error subclasses and direct Throwable subclasses
  • Apply the handle-or-declare rule to methods, constructors, lambdas and the implicit close() call
  • Trace the exact execution order of try, catch and finally, including nested statements, early returns and propagation across methods
  • Predict when a return, jump or throw in finally overrides a pending return value or exception
  • Write legal catch and multi-catch clauses and determine a multi-catch parameter's type
  • Use precise rethrow and exception chaining with getMessage(), toString(), getCause() and initCause()
  • Predict try-with-resources closing order, scope rules and which exception is primary or suppressed

The exception hierarchy: checked, unchecked and Error

Every exception in Java is a Throwable, and the compiler sorts them into two groups: unchecked types (RuntimeException, Error and their subclasses), which may be thrown anywhere, and checked types (every other Throwable, including Exception itself, IOException and Throwable), which must be handled or declared.

BranchChecked?Typical examplesMeaning
Error and subclassesNoStackOverflowError, OutOfMemoryError, ExceptionInInitializerErrorSerious JVM or linkage problems an application normally should not try to recover from
RuntimeException and subclassesNoNullPointerException, ArithmeticException, IllegalArgumentException (and its subclass NumberFormatException), IllegalStateException, ArrayIndexOutOfBoundsException, UncheckedIOExceptionProgramming errors and bad arguments
Any other Exception or ThrowableYesIOException, FileNotFoundException, NoSuchFileException, InterruptedException, TimeoutException, Exception, ThrowableConditions a caller is expected to plan for

Handle or declare. Wherever a checked exception can be thrown — a throw statement, a call to a method whose throws clause lists it, a constructor, or the hidden close() call of try-with-resources — it must be caught by an enclosing try or listed in the enclosing method's throws clause. This is a compile-time rule about what could happen: a throw new Exception(...) on a branch that never runs still fails with unreported exception Exception; must be caught or declared to be thrown.

Lambdas are judged against their functional interface. A lambda body is the implementation of the interface's single abstract method, so the only checked exceptions it may let escape are the ones that method declares. Supplier<T>.get(), Runnable.run(), Function.apply() and most java.util.function methods declare none, so Supplier<String> s = () -> readLine(); fails to compile when readLine() throws IOException. The enclosing method's throws clause and any try around the later call to get() are outside the body and do not count. Two fixes work: handle the exception inside the lambda body, or target an interface whose method declares it, such as Callable<V> (V call() throws Exception) or your own functional interface.

Error is not an Exception. Error and Exception are siblings under Throwable, so catch (Exception e) never catches an Error. Only catch (Error e), a matching Error subclass, or catch (Throwable t) does. A finally block still runs when an Error passes through. A failing static initializer is the classic case: if class Tariff { static double rate = Double.parseDouble("n/a"); } throws during class initialization, the first use of Tariff.rate receives an ExceptionInInitializerError whose getCause() is the original NumberFormatException. A catch (NumberFormatException e) around that use does not match; a catch (Error e) does.

Custom exceptions: choosing the superclass, messages and causes

A custom exception is an ordinary class that extends an exception type, and its superclass alone decides whether it is checked: extend Exception (or any checked subclass such as IOException) when callers must handle the condition, and extend RuntimeException (or a subclass like IllegalStateException) when it signals a programming error callers may ignore. Extending Error also makes it unchecked but is reserved for JVM-level problems; extending Throwable directly gives you a checked exception.

Custom exceptions normally add constructors that pass to Throwable's: super(), super(message), super(message, cause) and super(cause). Everything else is inherited:

  • getMessage() returns the message passed in, or null when none was given.
  • toString() — what println(e) and string concatenation use — returns the class's binary name, then ": " and the message only when the message is not null. A class in the unnamed package prints with its simple name; a JDK class prints fully qualified, e.g. java.lang.UnsupportedOperationException. A nested class prints with its binary name, such as Outer$Inner. toString() calls getLocalizedMessage(), which calls getMessage(), so overriding getMessage() changes what toString() and stack traces show, even if the constructor passed a different message to super.
  • getCause() returns null unless a cause was supplied.
class OvenException extends Exception {
    OvenException(String message) {
        super(message);
    }
}

class PantryException extends RuntimeException {
    PantryException(String message) {
        super(message);
    }
}

public class Main {
    static void bake(int minutes) throws OvenException {
        if (minutes > 90) throw new OvenException("too hot");
        if (minutes < 5) throw new PantryException("no flour");
        System.out.println("baked " + minutes);
    }

    public static void main(String[] args) {
        try {
            bake(30);
            bake(120);
        } catch (OvenException e) {
            System.out.println(e + " | " + e.getMessage()
                + " | " + e.getCause());
        }
    }
}

This prints baked 30, then OvenException: too hot | too hot | null. bake must declare OvenException because it is checked, while PantryException extends RuntimeException and needs no declaration; had bake(2) been called, it would have escaped main uncaught, because nothing catches it.

Exception chaining wraps a low-level exception in one that makes sense to the caller, without losing the original:

class RecipeException extends Exception {
    RecipeException(String message, Throwable cause) {
        super(message, cause);
    }

    RecipeException(Throwable cause) {
        super(cause);
    }
}

public class Main {
    static int servings(String text) throws RecipeException {
        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException e) {
            throw new RecipeException("unreadable servings", e);
        }
    }

    public static void main(String[] args) {
        try {
            servings("4x");
        } catch (RecipeException e) {
            System.out.println(e.getMessage());
            System.out.println(e.getCause());
        }
        var wrapped = new RecipeException(
            new ArithmeticException("/ by zero"));
        System.out.println(wrapped.getMessage());
        try {
            wrapped.initCause(new IllegalArgumentException());
        } catch (IllegalStateException e) {
            System.out.println("cause locked");
        }
    }
}

Output: unreadable servings, then java.lang.NumberFormatException: For input string: "4x", then java.lang.ArithmeticException: / by zero, then cause locked. Three rules show here: super(message, cause) keeps your message and makes the original reachable through getCause(); super(cause) alone sets the message to cause.toString() (class name included), not to the cause's own message; and a cause can be set only once — calling initCause() on an exception whose cause was already set (even by a constructor) throws IllegalStateException.

try/catch/finally: the order statements run in

When code in a try block throws, the rest of the try block is skipped, the first catch whose type matches the exception runs, then finally runs, and execution continues after the whole statement. With no exception, the catch blocks are skipped and finally still runs. If the try block executes return, finally runs before the method leaves and the statements after the try statement do not run.

public class Main {
    static void weigh(String kg) {
        try {
            System.out.print("try ");
            if (kg.isEmpty()) return;
            Integer.parseInt(kg);
            System.out.print("parsed ");
        } catch (NumberFormatException e) {
            System.out.print("catch ");
        } finally {
            System.out.print("finally ");
        }
        System.out.print("after ");
    }

    public static void main(String[] args) {
        weigh("12");
        System.out.println("|");
        weigh("kg");
        System.out.println("|");
        weigh("");
        System.out.println("|");
    }
}

Output:

try parsed finally after |
try catch finally after |
try finally |

The three calls show the normal path, a handled exception (parsed skipped, catch runs, finally runs, execution continues), and an early return (finally runs, after is skipped). A finally that only prints does not change the method's result; any statement after the try statement runs only if the try statement completed normally.

Shape rules the compiler enforces:

  • A plain try needs at least one catch or a finally; a try-with-resources may stand alone.
  • Order is fixed: try, any catches, then at most one finally last. A catch after finally does not compile.
  • Code that can never be reached is an error. If the try block and every catch end in return or throw (and finally completes normally), a statement after the try statement is unreachable and the method fails to compile.
  • Try statements nest. An inner try/finally with no matching catch runs its finally and passes the exception outward, where the nearest enclosing matching catch handles it; any statements between the inner statement and that catch are skipped.

Catch clauses: matching, ordering and what they may catch

A catch clause matches an exception if the thrown object is an instance of the catch type, and the clauses are tried top to bottom; only the first match runs. Three compile-time rules govern which catch clauses are legal:

RuleCompiles?Compiler message
A superclass catch before a subclass catch (IllegalArgumentException then NumberFormatException)No — the subclass catch is unreachableexception NumberFormatException has already been caught
A subclass catch before its superclass (EOFException then IOException, around a call that declares IOException)Yes — the more specific handler wins when it matches—
Catching a checked exception the try block cannot throw (catch (CloneNotSupportedException e) around x = x + 1;)Noexception CloneNotSupportedException is never thrown in body of corresponding try statement
Catching Exception, Throwable or any unchecked type around anythingYes — these could always be thrown—

The "never thrown" rule applies to checked types only, and a checked subclass of something the try block can throw is allowed: a call that declares IOException may really throw FileNotFoundException, so catch (FileNotFoundException e) is legal there.

What the try block can throw is judged from declarations, not from the object really thrown. Take class ShipException extends Exception, class CustomsException extends ShipException and class PermitException extends CustomsException, with static void dispatch() throws ShipException { throw new PermitException(); }. A caller must handle ShipException, the declared type: a try whose only handler is catch (CustomsException e) leaves ShipException unreported. At runtime, though, the actual class decides which catch runs: with catch (CustomsException e) followed by catch (ShipException e), the thrown PermitException lands in the first one. Listing the subclass and its superclass in one multi-catch is not an option (see the next section).

Catches guard the try block only. An exception thrown inside a catch block is not seen by the other catch clauses of the same statement; it runs the finally block and travels outward. A checked exception thrown from a catch block therefore needs an outer handler or a throws clause, even if a sibling catch (Exception e) sits right below it.

public class Main {
    static void refill() {
        try {
            throw new UnsupportedOperationException("first");
        } catch (UnsupportedOperationException e) {
            System.out.print(e.getMessage() + " ");
            throw new ArithmeticException("second");
        } catch (ArithmeticException e) {
            System.out.print("sibling ");
        } finally {
            System.out.print("cleanup ");
        }
    }

    public static void main(String[] args) {
        try {
            refill();
        } catch (RuntimeException e) {
            System.out.println(e.getClass().getSimpleName());
        }
    }
}

This prints first cleanup ArithmeticException: the ArithmeticException thrown inside the first catch bypasses the sibling catch (ArithmeticException e), finally prints cleanup, and the caller's catch (RuntimeException e) handles it.

A single-type catch parameter is an ordinary local variable, so it may be reassigned (which disables precise rethrow, below). Since Java 22 the parameter can be the unnamed variable _ when the handler does not need the exception, for example catch (ArithmeticException _) { return 0; }. _ stands only for the variable name: the exception type is still mandatory, and the unnamed variable can never be referenced, not even to read its message. It works in multi-catch too.

Multi-catch: one handler for unrelated exception types

A multi-catch clause, catch (A | B e), runs the same handler for several exception types, and it has three rules: the alternatives must not be related by subclassing, the parameter is implicitly final, and its static type is the alternatives' least upper bound.

  • No subclass pairs. catch (EOFException | IOException e) fails with Alternatives in a multi-catch statement cannot be related by subclassing, followed by Alternative EOFException is a subclass of alternative IOException — catch the superclass alone. The same goes for a pair such as ArrayIndexOutOfBoundsException | IndexOutOfBoundsException, and for listing Exception beside any other exception type. Judge by the real hierarchy, not by name: UncheckedIOException extends RuntimeException, not IOException, so it can share a multi-catch with any IOException type.
  • Implicitly final. You may read e or rethrow it, but any assignment to it fails with multi-catch parameter e may not be assigned. A single-type catch parameter has no such restriction.
  • Static type = least upper bound. For ArrayIndexOutOfBoundsException | ClassCastException the nearest common superclass is RuntimeException, so e can be assigned to RuntimeException, Exception or Throwable, but assigning it to ClassCastException or IndexOutOfBoundsException fails with incompatible types: RuntimeException cannot be converted to …. Only members every alternative shares are available: for EOFException | AccessDeniedException the type is IOException, so calling getFile() — which AccessDeniedException inherits from FileSystemException — fails with cannot find symbol. Interfaces that every alternative implements are part of the type too, and method calls still dispatch on the real object.
interface Retryable {
    int delaySeconds();
}

class BusyException extends Exception implements Retryable {
    public int delaySeconds() { return 30; }
}

class OfflineException extends Exception implements Retryable {
    public int delaySeconds() { return 300; }
}

public class Main {
    static void ping(boolean up) throws BusyException, OfflineException {
        if (up) throw new BusyException();
        throw new OfflineException();
    }

    public static void main(String[] args) {
        for (boolean up : new boolean[] {true, false}) {
            try {
                ping(up);
            } catch (BusyException | OfflineException e) {
                Retryable r = e;
                Exception ex = e;
                System.out.println(r.delaySeconds() + "s "
                    + ex.getClass().getSimpleName());
            }
        }
    }
}

Output: 30s BusyException, then 300s OfflineException. e is typed as Exception & Retryable, so it can be assigned to either type, and delaySeconds() runs the version of whichever class was actually thrown.

Ordering rules still apply: a multi-catch catches every subtype of each alternative, so a later single catch for a subclass of one of its alternatives is unreachable. After catch (IndexOutOfBoundsException | ClassCastException e), a following catch (ArrayIndexOutOfBoundsException e) fails with exception ArrayIndexOutOfBoundsException has already been caught. Put the specific catch first.

Rethrowing: precise rethrow from catch (Exception e)

Precise rethrow (Java 7+) means that throw e; inside catch (Exception e) is treated as throwing only the checked exceptions the try block can actually throw, provided the catch parameter is final or effectively final. That lets a method log-and-rethrow with a broad catch and still declare a narrow throws clause.

import java.io.EOFException;

public class Main {
    static void readHeader(byte[] data) throws EOFException {
        try {
            if (data.length < 4) {
                throw new EOFException("short header");
            }
            System.out.println("header ok");
        } catch (Exception e) {
            System.out.println("rejecting: " + e.getMessage());
            throw e;
        }
    }

    public static void main(String[] args) {
        try {
            readHeader(new byte[8]);
            readHeader(new byte[2]);
        } catch (EOFException e) {
            System.out.println("caller got "
                + e.getClass().getSimpleName());
        }
    }
}

Output: header ok, rejecting: short header, caller got EOFException. It compiles with only throws EOFException because the try block can throw nothing else checked; the rethrown object is the original EOFException. The rules around it:

  • Every checked type the try block can throw must still be declared. If the try block could also throw CloneNotSupportedException, the method would need both types (or supertypes that cover them); declaring only one fails.
  • Assigning e switches it off. Once the catch block assigns anything to e — even another EOFException — the parameter is no longer effectively final, so throw e throws its declared type Exception and the narrow throws clause fails with unreported exception Exception. Declaring the parameter final keeps precise rethrow working (and forbids the assignment).
  • Adding a new checked call breaks it. If the try block gains a call that throws a wider checked type — say Files.readAllBytes(path), which declares IOException — the rethrow can now throw IOException and a throws EOFException clause no longer compiles. Widening the clause to a supertype is always fine.
  • With only unchecked exceptions possible, throw e from catch (Exception e) needs no throws clause at all — it simply rethrows the original object.

finally and return values: what finally can and cannot change

A return in try or catch evaluates its expression before finally runs, and that value is then held while finally executes. Finally can change variables and the objects they point to, but not the value already chosen — unless finally completes abruptly itself.

import java.util.ArrayList;
import java.util.List;

public class Main {
    static int level = 3;

    static int raise() {
        try {
            return level * 2;
        } finally {
            level = 100;
        }
    }

    static List<String> collect() {
        List<String> items = new ArrayList<>(List.of("x"));
        try {
            items.add("y");
            return items;
        } finally {
            items.add("z");
            items = List.of("ignored");
        }
    }

    @SuppressWarnings("finally")
    static String status(String code) {
        try {
            return "code " + Integer.parseInt(code);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("rejected");
        } finally {
            return "fallback";
        }
    }

    public static void main(String[] args) {
        System.out.println(raise() + " " + level);
        System.out.println(collect());
        System.out.println(status("A7"));
    }
}

This prints 6 100, [x, y, z] and fallback:

  • raise() returns 6 — level * 2 computed when return ran — even though finally then sets level to 100. A primitive return value is a copy.
  • collect() returns the original list. Finally added z to that same object (so the caller sees it) and then pointed the local variable at another list, which does not affect the reference already being returned.
  • status() returns fallback: a return in finally replaces the pending result, and here it silently discards the IllegalArgumentException the catch block threw, so the caller never sees it.

Abrupt completion in finally always wins. A return, break, continue or throw in finally overrides whatever the try or catch was doing — a pending return value or a pending exception. A continue in finally inside a loop, for instance, swallows the iteration's exception and moves on to the next element. Similarly, an exception thrown from finally replaces the one in flight, and a plain try/finally does not record the lost exception anywhere: getSuppressed() on the new exception has length 0. Suppressed exceptions are a try-with-resources feature only. javac compiles all of these, with at most a finally clause cannot complete normally lint warning.

Propagation: how an uncaught exception unwinds the stack

An exception that no catch in the current method matches propagates to the caller, running each finally it passes through from the innermost outward, until a matching catch handles it; if none does, the thread ends and the default handler prints Exception in thread "main" … with the stack trace. Every statement between the throw point and the handler is skipped.

public class Main {
    static void pack() {
        try {
            throw new UnsupportedOperationException("fragile");
        } catch (ClassCastException e) {
            System.out.print("pack-catch ");
        } finally {
            System.out.print("pack-finally ");
        }
        System.out.print("packed ");
    }

    static void ship() {
        try {
            pack();
            System.out.print("shipped ");
        } catch (UnsupportedOperationException e) {
            System.out.print("ship-catch ");
            throw new IllegalStateException("returned", e);
        } finally {
            System.out.print("ship-finally ");
        }
    }

    public static void main(String[] args) {
        try {
            ship();
        } catch (IllegalStateException e) {
            System.out.println("main " + e.getMessage()
                + " <- " + e.getCause().getMessage());
        }
    }
}

This prints pack-finally ship-catch ship-finally main returned <- fragile. In pack() the non-matching catch (ClassCastException e) is skipped, but its finally runs before the exception leaves the method, and packed never prints. Back in ship(), shipped is skipped too; the catch wraps the exception in a new one, that new exception passes through ship()'s finally, and main handles it with the original still reachable through getCause(). Unwinding skips mismatched catches but never skips a finally, and an inner finally always runs before an outer catch.

Only a catch stops an exception. A chain of try/finally blocks with no matching catch runs every finally on the way out and then terminates the thread with the original exception; code after the call in each method, and after the call in main, never runs.

try-with-resources: opening, closing and scope

Try-with-resources (try (Type r = ...) { }) automatically calls close() on every resource that implements AutoCloseable (including Closeable), whether the body completes normally, returns or throws. Resources are initialized left to right and closed right to left, right after the body and before any catch or finally of the same statement runs.

class Valve implements AutoCloseable {
    private final String name;

    Valve(String name) {
        this.name = name;
        System.out.print("open " + name + " ");
    }

    @Override
    public void close() {
        System.out.print("shut " + name + " ");
    }
}

public class Main {
    public static void main(String[] args) {
        Valve mains = new Valve("mains");
        try (mains; Valve hot = new Valve("hot");
             var _ = new Valve("cold")) {
            System.out.print("flow ");
            throw new ArithmeticException("leak");
        } catch (ArithmeticException e) {
            System.out.print("patched ");
        } finally {
            System.out.println("off");
        }
    }
}

This prints open mains open hot open cold flow shut cold shut hot shut mains patched off. The catch sees resources that are already closed. When try-with-resources statements are nested, the inner one closes its resources, then runs its own catch and finally, before the outer one closes and does the same.

Rules for resources:

  • Resource variables are implicitly final and scoped to the try block. Assigning a declared resource inside the body fails to compile, and using the variable after the statement fails with cannot find symbol.
  • Existing variables (Java 9+). The header may name an existing local variable, parameter or field that is final or effectively final, as mains is above; it is closed like a declared resource. A local variable that is assigned again anywhere in the method — before or after the try statement — is not effectively final, and a non-final field is not allowed either. The header holds only declarations and variable names, never another expression such as a bare constructor call.
  • A null resource is allowed and simply not closed; no NullPointerException is thrown by the statement itself.
  • Unnamed resources (Java 22+). var _ = new Valve("cold") declares a resource the body never refers to; it is still opened and closed. A header may declare more than one unnamed resource, but each still needs its type or var: _ replaces only the name.
  • Checked exceptions from close() count. The implicit close() call is checked against the declared type of the resource variable. AutoCloseable.close() declares throws Exception and Closeable.close() declares throws IOException, so if your class's close() keeps such a clause, or the variable's declared type is AutoCloseable or Closeable, the statement fails with unreported exception … and the note exception thrown from implicit call to close() on resource variable …. Only a catch for that checked type (or a supertype) or a matching throws clause fixes it; a finally block or a catch for an unchecked type does not.
  • Some JDK classes narrow close(). An overriding close() may declare fewer exceptions, and then nothing needs handling. StringReader, CharArrayReader, CharArrayWriter, Scanner and Formatter all declare close() with no throws clause, so try (var r = new StringReader("text")) { } compiles with no catch. Do not assume every in-memory class does: StringWriter, ByteArrayInputStream and ByteArrayOutputStream still declare close() throws IOException.

try-with-resources failures: suppressed exceptions and which one wins

In try-with-resources the first exception thrown stays primary, and any exception thrown by a later close() is attached to it as a suppressed exception, read back with getSuppressed() in closing order. Every resource is still closed even when an earlier close fails.

class Hatch implements AutoCloseable {
    private final String side;

    Hatch(String side) {
        if (side.isBlank()) {
            throw new IllegalArgumentException("unnamed hatch");
        }
        this.side = side;
    }

    @Override
    public void close() {
        throw new IllegalStateException("jam " + side);
    }
}

public class Main {
    static void show(Exception e) {
        StringBuilder line = new StringBuilder(e.getMessage());
        for (Throwable s : e.getSuppressed()) {
            line.append(" + ").append(s.getMessage());
        }
        System.out.println("[" + line + "]");
    }

    public static void main(String[] args) {
        try (var fore = new Hatch("fore"); var aft = new Hatch("aft")) {
            throw new UnsupportedOperationException("overflow");
        } catch (RuntimeException e) {
            show(e);
        }
        try (var fore = new Hatch("fore"); var aft = new Hatch("aft")) {
            System.out.print("sealed ");
        } catch (RuntimeException e) {
            show(e);
        }
        try (var fore = new Hatch("fore"); var aft = new Hatch(" ")) {
            System.out.print("sealed ");
        } catch (RuntimeException e) {
            show(e);
        }
    }
}

Output:

[overflow + jam aft + jam fore]
sealed [jam aft + jam fore]
[unnamed hatch + jam fore]
What failsPrimary exceptionSuppressed
Body throws, then closes throwThe body's exceptionEvery close failure, in closing order (reverse of declaration)
Body succeeds, closes throwThe first close() failure (the last-declared resource)Later close failures
A resource initializer throwsThe initializer's exception — the body never runsClose failures of resources already initialized (only those are closed)

A failing close() also defeats a return in the body: the return value is computed, close() runs, and its exception propagates instead of the value. And if a resource's constructor throws, only the resources to its left are closed; the failed one never existed, so its close() is never called. A catch or finally on the same statement runs after all of this. Contrast plain try/finally, where a second exception replaces the first and nothing is suppressed.

Tip. Objective 4.1 is examined through short, complete programs that ask for the exact output, whether the code compiles, or which exception escapes at runtime. Flow questions place print statements in try, catch, finally and in nested or calling methods to test execution order, early returns, propagation and what finally does to a pending return or exception. Compile questions offer several catch, multi-catch, throws-clause or resource-header variants and ask which ones are legal, so the rules on catch ordering, unthrown checked exceptions, subclass-related alternatives, final parameters, precise rethrow and effectively final resources matter. Try-with-resources questions combine failing bodies, constructors and close() methods and ask which exception is primary, what getSuppressed() holds and in what order resources close. Custom-exception questions turn on the chosen superclass and on what getMessage(), toString() and getCause() return.

Key takeaways
  • Unchecked means RuntimeException or Error (and subclasses); every other Throwable, including Exception and Throwable itself, is checked and must be caught or declared.
  • catch (Exception e) never catches an Error; finally still runs for one, and a failing static initializer surfaces as ExceptionInInitializerError with the original as its cause.
  • Catches are tried top to bottom: a superclass before its subclass fails to compile, and catching a checked exception the try block cannot throw fails too.
  • An exception thrown inside a catch block goes outward through finally, never to a sibling catch.
  • Multi-catch alternatives must be unrelated, the parameter is implicitly final, and its type is the alternatives' common supertype.
  • A return value is fixed before finally runs; a return, continue, break or throw in finally overrides it or any pending exception, and plain try/finally suppresses nothing.
  • Try-with-resources closes right to left, before its own catch and finally run; resource variables are final and scoped to the try block, and null resources are skipped.
  • The first exception thrown in try-with-resources is primary; later close() failures are suppressed onto it, and a failed initializer closes only the resources before it.
  • A custom exception's toString() is ClassName: message, getMessage() is null when no message was passed, and super(cause) alone copies cause.toString() into the message.

Frequently asked questions

What is the difference between checked and unchecked exceptions in Java?

Unchecked exceptions are RuntimeException, Error and their subclasses; the compiler lets you throw them anywhere without handling. Every other Throwable, including Exception, IOException and Throwable itself, is checked: wherever one can be thrown it must be caught by an enclosing try or listed in the method's throws clause, or the code does not compile.

Does finally run if the try block returns?

Yes. When try or catch executes return, the return expression is evaluated first, then finally runs, then the method returns that saved value. Finally can change variables or mutate a returned object, but it cannot change the saved value unless finally itself executes return, throw, break or continue, which overrides the pending result.

What happens if both the try block and finally throw an exception?

In a plain try/finally, the exception thrown by finally replaces the original, which is lost: it is not recorded as suppressed. Only try-with-resources keeps both, making the body's exception primary and attaching close() failures to it through getSuppressed().

In what order does try-with-resources close resources?

Resources are initialized in the order they are declared and closed in the reverse order, immediately after the try block ends and before any catch or finally of the same statement runs. If a resource's initializer throws, the body is skipped and only the resources already initialized are closed.

Why can't a multi-catch list a subclass and its superclass?

Java forbids alternatives in a multi-catch that are related by subclassing, because catching the superclass already covers the subclass. A clause such as catch (EOFException | IOException e) is rejected with the message that alternatives in a multi-catch statement cannot be related by subclassing; catch the superclass alone, or use separate catch clauses with the subclass first.

Can you reassign the exception variable in a catch block?

In a single-type catch, yes: the parameter is an ordinary local variable, although assigning it disables precise rethrow. In a multi-catch, no: the parameter is implicitly final and any assignment is a compile error. Try-with-resources variables are implicitly final as well.

How do I create a custom checked exception in Java?

Declare a class that extends Exception (or another checked type such as IOException) and add constructors that call super(message), super(message, cause) or super(cause). Extending RuntimeException instead makes it unchecked. The class inherits getMessage(), toString() and getCause() from Throwable.

What is precise rethrow in Java?

If a catch (Exception e) block rethrows e with throw e and never assigns e, the compiler treats the rethrow as throwing only the checked exceptions the try block can actually throw. The method can then declare those narrow types instead of Exception. Reassigning e, or adding a call that throws a new checked type, removes that benefit.

Source

This lesson covers the "Implementing Exception Handling in Java Applications" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.

Test yourself on this topic
1Z0-831 practice questions, with full explanations.
Practice now

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?