Command Palette

Search for a command to run...

PHASE 7Intermediate Java 7+ ~31 min· topic 7 of 8

Topic 7.7

Multi-catch and Rethrowing

In one line

A multi-catch, catch (A | B e), handles several unrelated exception types with one block. Rethrowing passes an exception on after doing something with it, and Java 7's precise rethrow lets you catch broadly while still declaring only the exceptions that can really occur.

Think of it like this

A shop's returns desk. Whether the item is broken, the wrong size or the wrong colour, the desk does the same thing: refund and say sorry. There's one desk for all three reasons, not three identical desks. But if the problem is "the item was stolen", the desk writes a note and passes it on to the manager. Multi-catch is the single desk for several reasons; rethrowing is passing the case on.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Multi-catch
One catch block for several exception types, written catch (TypeA | TypeB e).
Alternative
One of the types listed in a multi-catch, separated by |.
Least upper bound
The most specific type that all the alternatives share as a parent. It's the type the compiler gives the multi-catch variable.
Rethrow
Throwing the exception you just caught again, so it keeps travelling up to the caller.
Precise rethrow
Java 7's rule that a rethrown, unchanged catch parameter only counts as the checked exceptions the try block can really throw.
Wrapping
Throwing a new exception with the caught one as its cause, instead of rethrowing the original.
Log and rethrow
Logging an exception and then rethrowing it. Done at every layer, the same error is logged many times.
Retry
Trying an operation again after it fails, usually a fixed number of times, because some failures (like timeouts) are temporary.

Step by step

01Two identical handlers, before Java 7

Copy-pasted catch blocks drift apart over time: someone fixes a bug in one and forgets the other. Catching Exception instead is worse: it also catches NullPointerException and other bugs you never meant to handle.

Main.javawhole filejava
try {
    total = Integer.parseInt(a) / Integer.parseInt(b);
} catch (NumberFormatException e) {
    System.out.println("bad input: " + e.getMessage());
    total = 0;
} catch (ArithmeticException e) {
    System.out.println("bad input: " + e.getMessage());   // same code, twice
    total = 0;
}

02One handler with multi-catch

List the types with |. The block runs for either. The variable's type is their closest common parent, RuntimeException here, so getMessage() and every other RuntimeException method is available.

If you need to know which one it was, e instanceof ArithmeticException still works. But if the handling differs much, separate catch blocks are clearer.

Main.javawhole filejava
try {
    total = Integer.parseInt(a) / Integer.parseInt(b);
} catch (NumberFormatException | ArithmeticException e) {   // e is RuntimeException, and final
    System.out.println("bad input: " + e.getMessage());
    total = 0;
}

03The two compiler rules

Rule 1: alternatives can't be subclasses of one another. The narrower one would be redundant, and javac tells you which one.

Rule 2: the parameter is final. Allowing e = ... would let you assign, say, an ArithmeticException to a variable that might be holding a NumberFormatException, which makes the type rules messy, so Java forbids it.

terminal
$ javac Main.java
── expected output ──
Main.java:6: error: Alternatives in a multi-catch statement cannot be related by subclassing
} catch (FileNotFoundException | IOException e) {
^
Alternative FileNotFoundException is a subclass of alternative IOException
1 error

04How it compiles

javac emits the handler's code once, and adds one row to the exception table per alternative, all pointing at the same target. So multi-catch makes class files smaller than duplicated catch blocks, as well as source code.

How it compilesdiagram
Rendering diagram…

05Rethrow: act, then pass it on

Sometimes a method must react to a failure without handling it: roll back a transaction, release a reservation, count an error. Catch, do the work, and throw e; to let the caller see the same exception.

The stack trace is not reset: it was captured when the exception was created. That's why rethrowing the same object is better than throw new RuntimeException(e.getMessage()), which loses the type and the trace.

Main.javawhole filejava
void transfer(Account from, Account to, long amount) {
    reserve(from, amount);
    try {
        credit(to, amount);
    } catch (RuntimeException e) {
        release(from, amount);   // undo our part
        throw e;                 // the caller still decides what the failure means
    }
}

06Precise rethrow (Java 7)

Here the catch is for Exception, but the method declares only FileNotFoundException and TimeoutException. Before Java 7 that was a compile error: the static type of e is Exception, so throw e needed throws Exception.

Java 7 is smarter: because e is effectively final and is the catch parameter, the compiler knows it can only hold one of the checked exceptions the try block throws, or an unchecked one. Reassign e and the old rule comes back.

Main.javawhole filejava
static void run(int n) throws FileNotFoundException, TimeoutException {
    try {
        step(n);                    // declared: throws FileNotFoundException, TimeoutException
    } catch (Exception e) {
        log(e);
        throw e;                    // OK since Java 7: precise rethrow
    }
}
terminal
$ javac Main.java # after adding e = new IOException("wrapped"); before the throw
── expected output ──
Main.java:11: error: unreported exception Exception; must be caught or declared to be thrown
throw e;
^
1 error

07Rethrow, wrap, or handle?

Handle it when you can actually fix the situation (use a default, retry, ask again). Rethrow when you need to clean up but the exception already means the right thing to the caller. Wrap when crossing a layer boundary or adding context the caller needs.

Log exactly once, at the place that finally handles the exception (often a top-level request handler). Logging and rethrowing at every layer is the classic way to get five copies of the same stack trace for one failure.

Rethrow, wrap, or handle?diagram
Rendering diagram…

Try it yourself

  1. 1

    Find the common type

    In the first example, add IndexOutOfBoundsException to the multi-catch and pass {"10"} (a one-element array) as an input. Predict what's printed (careful: the catch block itself reads in[1]!). Then run and explain.

  2. 2

    Turn precise rethrow off

    In the precise rethrow example, add e = new Exception("replaced"); before throw e;. Predict the compiler error. Then change the method to throws Exception and see which other line now fails to compile, and why.

  3. 3

    Change the retry budget

    Call withRetry("cake", 4) after resetting calls. Predict how many attempt lines print and whether it succeeds.

Code & diagrams

One handler for two unrelated exceptions Java 7+ New tab

When the division fails, nothing is printed for that pair: the exception escapes while the string is still being built.

Sign in to run this example in your browser.

Expected output

10/2 = 5
bad input 10/zero -> NumberFormatException
bad input 10/0 -> ArithmeticException
bad input null/1 -> NumberFormatException
Precise rethrow and the multi-catch variable's type Java 7+ New tab
Sign in to run this example in your browser.

Expected output

step 0 ok
log: step 1 failed with FileNotFoundException
recoverable: config.yml
log: step 2 failed with TimeoutException
recoverable: db took 5s
log: step 3 failed with IllegalStateException
bug: bug in step 3
Retry, then wrap with the history attached New tab

Real retry code also waits between attempts (with growing delays) and retries only failures that are known to be temporary.

Sign in to run this example in your browser.

Expected output

attempt 1 failed: timeout on attempt 1
attempt 2 failed: timeout on attempt 2
tea=42
attempt 1 failed: timeout on attempt 1
attempt 2 failed: timeout on attempt 2
gave up after 2 attempts, cause: timeout on attempt 2
earlier failures kept: 1
Rethrow vs wrap vs the anti-patterns (fragment)java
// Rethrow: same object, original type and stack trace
catch (SQLException e) {
    connection.rollback();
    throw e;
}

// Wrap: new abstraction, original kept as the cause
catch (SQLException e) {
    throw new OrderStorageException("could not save order " + id, e);
}

// Anti-pattern 1: log and rethrow at every layer (duplicate traces in the logs)
catch (SQLException e) {
    log.error("save failed", e);
    throw e;
}

// Anti-pattern 2: rethrow as a new exception without the cause (trace and type lost)
catch (SQLException e) {
    throw new RuntimeException(e.getMessage());
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Related alternatives

Write catch (FileNotFoundException | IOException e).

terminal
$ javac Main.java
── what you'll see ──
Main.java:6: error: Alternatives in a multi-catch statement cannot be related by subclassing
} catch (FileNotFoundException | IOException e) {
^
Alternative FileNotFoundException is a subclass of alternative IOException
1 error

Break #2

Assign to the multi-catch parameter

Inside catch (NumberFormatException | IllegalStateException e), write e = new IllegalStateException();.

terminal
$ javac Main.java
── what you'll see ──
Main.java:6: error: multi-catch parameter e may not be assigned
e = new IllegalStateException();
^
1 error

Break #3

Reassign before rethrowing

In catch (Exception e) of a method declared throws IOException, write e = new IOException("wrapped"); throw e;.

terminal
$ javac Main.java
── what you'll see ──
Main.java:11: error: unreported exception Exception; must be caught or declared to be thrown
throw e;
^
1 error

Myth vs fact

Myth

catch (A | B e) gives e the type of whichever one was thrown.

Fact

At compile time e has the least upper bound type of the alternatives (for example RuntimeException). At run time the object is still its real class, so getClass() and instanceof tell you which one it was.

Myth

Rethrowing an exception resets its stack trace to the rethrow line.

Fact

The trace was filled in when the object was constructed. throw e; leaves it as it was; only e.fillInStackTrace() would reset it, which you almost never want.

Myth

Catching Exception and rethrowing it forces the method to declare throws Exception.

Fact

Not since Java 7. With an effectively final catch parameter, precise rethrow lets you declare just the checked exceptions the try block can really throw.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The type of a multi-catch parameter is formally the union type A | B, a type you can't write anywhere else in Java. For member access it behaves as the least upper bound lub(A, B), which can be an intersection type, for example when both alternatives implement the same extra interface.

  • ▸

    Precise rethrow (JLS 11.2.2) says a throw of a final or effectively final catch parameter can throw exactly the exception classes that the try block can throw, that are assignable to the parameter's declared type, and that aren't caught by an earlier catch of the same statement. Earlier handlers in the same try therefore narrow what is rethrown.

  • ▸

    Rethrowing inside a catch makes the original exception's trace show the original throw site, while the catch site itself doesn't appear. If you need to record where it passed through, add a suppressed exception or wrap it; Java has no 'rethrow trace' feature.

  • ▸

    Retries need more than a loop in production: exponential backoff with jitter, a deadline, retrying only idempotent operations, and a circuit breaker to stop hammering a dependency that is down. Libraries like Resilience4j implement these (see the System Design course for reliability patterns).

Remember this

  1. 1

    Before Java 7, handling two exceptions the same way meant two identical catch blocks, or catching a broad parent type like Exception and accidentally catching bugs too. catch (NumberFormatException | ArithmeticException e) (Java 7) lists the alternatives with |. If any of them matches, the one block runs.

  2. 2

    The alternatives must not be related by subclassing. catch (FileNotFoundException | IOException e) is an error, because FileNotFoundException is already an IOException, so the first alternative adds nothing. Just catch IOException.

  3. 3

    Inside a multi-catch, the parameter e is implicitly final: you can't assign to it. Its static type is the least upper bound (the closest common supertype) of the alternatives. For NumberFormatException | ArithmeticException that's RuntimeException, so you can call RuntimeException's methods, but not anything specific to one alternative without an instanceof check.

  4. 4

    Rethrowing means a catch block throws the same exception again with throw e;. It's useful when you need to do something on the way out (record a metric, roll back, add a suppressed exception) but the caller should still decide what the failure means. Rethrowing the same object keeps its original stack trace; it's not reset to the rethrow line.

  5. 5

    Precise rethrow (Java 7): if you catch a broad type such as Exception and rethrow the catch parameter unchanged, the compiler knows the exception can only be one of the checked exceptions the try block can actually throw (plus any unchecked ones). So the method can declare throws FileNotFoundException, TimeoutException instead of throws Exception. This works only if the catch parameter is effectively final; reassign it and the compiler falls back to its declared type.

  6. 6

    The alternative to rethrowing is wrapping: throw new ServiceException("...", e); (Topic 7.5). Rethrow when the exception already means the right thing to the caller; wrap when you're crossing a layer boundary or adding context. Never do both, and avoid the log-and-rethrow habit, where every layer logs the same exception before rethrowing it, filling logs with duplicate stack traces.

Explain it without notes

01

What is multi-catch, and what are its restrictions?

02

What is the static type of a multi-catch parameter, and why is it final?

03

What is precise rethrow, and when does it stop working?

04

When should you rethrow, wrap, or handle an exception?

Practice

01

Write a method describe(Object o) that does ((String) o).charAt(5) and uses one multi-catch for ClassCastException | StringIndexOutOfBoundsException to return "invalid: " + simple class name. Test with "hello world", 42 and "hi".

02

Write static void load(String name) throws java.io.IOException, InterruptedException whose body calls helpers declared to throw those two, catches Exception e, prints cleanup for <name>, and rethrows. Show it working for a name that triggers each failure.

03

Write a withRetry that retries only IllegalStateException (temporary) but rethrows IllegalArgumentException (permanent) immediately. Show both behaviours.

Trade-offs

  • ↔

    Multi-catch removes duplication, but if the handling for each type should differ even slightly, separate catch blocks are clearer than instanceof checks inside one block.

  • ↔

    Catching Exception with precise rethrow is concise for clean-up-and-rethrow, but it also catches runtime exceptions (bugs); that's fine when you rethrow everything, dangerous if the block might swallow some.

  • ↔

    Rethrowing keeps the original type and trace, which is simple and honest; wrapping adds context and hides implementation details but deepens the chain. Pick per boundary, not per habit.

Done when you can

  • Done when you can replace duplicated catch blocks with a multi-catch and know its two compiler rules.

  • Done when you can say the static type of a multi-catch variable for any pair of exceptions.

  • Done when you can use precise rethrow and explain why reassigning the parameter breaks it.

  • Done when you can choose between handling, rethrowing and wrapping for a given layer.

  • Done when you can write a bounded retry loop that keeps the history of failures.