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.
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.
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.
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.
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.
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.
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
}
}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.
Try it yourself
- 1
Find the common type
In the first example, add
IndexOutOfBoundsExceptionto the multi-catch and pass{"10"}(a one-element array) as an input. Predict what's printed (careful: the catch block itself readsin[1]!). Then run and explain. - 2
Turn precise rethrow off
In the precise rethrow example, add
e = new Exception("replaced");beforethrow e;. Predict the compiler error. Then change the method tothrows Exceptionand see which other line now fails to compile, and why. - 3
Change the retry budget
Call
withRetry("cake", 4)after resettingcalls. Predict how many attempt lines print and whether it succeeds.
Code & diagrams
When the division fails, nothing is printed for that pair: the exception escapes while the string is still being built.
Expected output
10/2 = 5
bad input 10/zero -> NumberFormatException
bad input 10/0 -> ArithmeticException
bad input null/1 -> NumberFormatExceptionExpected 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 3Real retry code also waits between attempts (with growing delays) and retries only failures that are known to be temporary.
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: 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).
Break #2
Assign to the multi-catch parameter
Inside catch (NumberFormatException | IllegalStateException e), write e = new IllegalStateException();.
Break #3
Reassign before rethrowing
In catch (Exception e) of a method declared throws IOException, write e = new IOException("wrapped"); throw e;.
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 boundlub(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
throwof 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
Before Java 7, handling two exceptions the same way meant two identical catch blocks, or catching a broad parent type like
Exceptionand accidentally catching bugs too.catch (NumberFormatException | ArithmeticException e)(Java 7) lists the alternatives with|. If any of them matches, the one block runs. - 2
The alternatives must not be related by subclassing.
catch (FileNotFoundException | IOException e)is an error, becauseFileNotFoundExceptionis already anIOException, so the first alternative adds nothing. Just catchIOException. - 3
Inside a multi-catch, the parameter
eis implicitly final: you can't assign to it. Its static type is the least upper bound (the closest common supertype) of the alternatives. ForNumberFormatException | ArithmeticExceptionthat'sRuntimeException, so you can callRuntimeException's methods, but not anything specific to one alternative without aninstanceofcheck. - 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
Precise rethrow (Java 7): if you catch a broad type such as
Exceptionand 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 declarethrows FileNotFoundException, TimeoutExceptioninstead ofthrows Exception. This works only if the catch parameter is effectively final; reassign it and the compiler falls back to its declared type. - 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
What is multi-catch, and what are its restrictions?
What is the static type of a multi-catch parameter, and why is it final?
What is precise rethrow, and when does it stop working?
When should you rethrow, wrap, or handle an exception?
Practice
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".
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.
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
instanceofchecks inside one block. - ↔
Catching
Exceptionwith 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.