Java Multiple Exceptions
›Short versionOpen if you only have a minute.
- One try can fail in more than one way. Each catch clause is a different response. Only the first matching clause runs.
- Separate clauses when the recoveries differ: reject bad input in one, retry a transient failure in another.
- A multi-catch, written with |, is one response shared by several types. The alternatives cannot be related by subclassing.
- The multi-catch parameter is implicitly final. Its static type is the least upper bound of the alternatives, so you only get the methods they share.
- A trailing catch of Exception also matches unchecked bugs. Keep that width for a boundary that turns any leftover failure into one response.
A region of code rarely has only one way to fail. Parsing can reject the text. Arithmetic can reject a zero. A file can be missing. Several on one try let those failures take different paths. A is the other shape: several types, one path, because the response really is the same.
Different failures, different answers
calculate turns text into a number and divides. Those are two contracts. Bad text is a NumberFormatException. A zero is an ArithmeticException. The person who typed the text needs a different sentence for each.
static int calculate(String raw) {
try {
int number = Integer.parseInt(raw);
return 100 / number;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Not an integer: " + raw, e);
} catch (ArithmeticException e) {
throw new IllegalArgumentException("Divisor was zero", e);
}
}If raw is "nope", parseInt throws, the first clause runs, and the division never happens. If raw is "0", parsing succeeds and the second clause runs. The two types are . NumberFormatException extends IllegalArgumentException. ArithmeticException extends RuntimeException directly. Neither is a subtype of the other, so either clause could have been written first and both would still be reachable. Put the one a reader should meet first where the story of the method puts it. When one type is a parent of the other, order stops being a matter of taste. The catch-ordering page is that rule.
Each clause wraps the original as the cause. The caller sees a single unchecked type with a sentence, and the log still has the parse failure or the division failure underneath. Both clauses throw, so calculate does not continue after a failure. If a clause handled the problem and finished normally, the method would continue after the try statement, and only that one clause would have run.
One answer for several types
When the response does not depend on which of two siblings you got, repeating the block is noise. Multi-catch names both types and binds one .
static int calculate(String raw) {
try {
int number = Integer.parseInt(raw);
return 100 / number;
} catch (NumberFormatException | ArithmeticException e) {
throw new IllegalArgumentException("Bad divisor: " + raw, e);
}
}e is implicitly final. e = null does not compile. You may call methods that exist on every alternative. getMessage is on Throwable, so it is safe. The compile-time type of e is the least upper bound of the alternatives. For these two, that bound is RuntimeException, the nearest common parent. A method declared only on NumberFormatException is not visible on e unless you test the runtime type.
The alternatives have to be . FileNotFoundException | IOException is rejected, because FileNotFoundException is already an IOException. The compiler's message is that alternatives in a multi-catch cannot be related by subclassing. Use a single catch (IOException e) when every I/O failure shares a handler, or separate clauses when a missing file needs its own sentence and other I/O failures need another.
The clause that means everything else
A last clause of Exception matches what the earlier clauses left, including NullPointerException and the other unchecked bugs. At the edge of a request that is sometimes the response you want: one log, one answer to the caller, and the request ends.
try {
checkout(order);
} catch (ValidationException e) {
return badRequest(e.getMessage());
} catch (PaymentDeclinedException e) {
return paymentRequired("Card was declined");
} catch (Exception e) {
logger.error("Unexpected checkout failure for {}", order.id(), e);
return serverError("Please try again later");
}Validation and a declined card are outcomes the method knows. The last clause is the residue. Inside a library method, that residue usually means a bug you just hid from the caller. Let it propagate until a boundary that already planned a single answer for unexpected failure. Catching Throwable there would also take Error. The errors page is why that extra width is a process-level decision.
You handle a missing config file by writing a default, and you handle every other IOException by failing the startup. Can those two share a multi-catch?
They need two clauses. The actions differ, and FileNotFoundException is a subclass of IOException, so a multi-catch of both types does not compile. Catch FileNotFoundException first and write the default. Catch IOException second and fail startup. The first clause is the specific file. The second is every other I/O failure, which still includes subclasses such as an access denial, and no longer includes the missing file, because that one was already handled.