Java Multiple Catch Blocks
Why One try Needs Several Handlers
A single block of code can fail in more than one way, and each failure usually needs a different response. Reading a number from a file can fail because the file is missing (tell the admin), because the text isn't a number (report the bad value), or because the value is out of range (reject the configuration). One generic handler would have to treat all of these the same way.
Java solves this in two ways:
- Several
catchblocks after onetry, each handling a different exception type differently. - A multi-catch block (Java 7+), where one
catchhandles several types with the same code, separated by a vertical bar (|).
Several catch Blocks
try {
// statements
} catch (FirstType e) {
// handle FirstType
} catch (SecondType e) {
// handle SecondType
} catch (ThirdType e) {
// handle ThirdType
}
Example — dividing two values supplied as text:
public class Ratio {
static void ratio(String[] parts) {
try {
int a = Integer.parseInt(parts[0]);
int b = Integer.parseInt(parts[1]);
System.out.println(a + " / " + b + " = " + (a / b));
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero");
} catch (NumberFormatException e) {
System.out.println("Not a number: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Two values are required");
}
}
public static void main(String[] args) {
ratio(new String[] {"10", "2"});
ratio(new String[] {"10", "0"});
ratio(new String[] {"10", "x"});
ratio(new String[] {"10"});
}
}
Output:
10 / 2 = 5
Cannot divide by zero
Not a number: For input string: "x"
Two values are required
How Java chooses a handler:
- When an exception is thrown in the
tryblock, Java checks thecatchclauses from top to bottom. - The first clause whose type matches (the same class or a superclass of the thrown exception) runs.
- Only one
catchblock ever runs for a given exception. The remaining clauses are skipped, and execution continues after the whole statement. - If no clause matches, the exception propagates to the caller.
In the third call, parseInt("x") throws NumberFormatException. Java tests ArithmeticException first — no match — then NumberFormatException — match — and runs that block. In the fourth call, parts[1] doesn't exist, so the third handler runs.
Each handler declares its own variable. Using the name e in every block is allowed because each catch parameter is scoped to its own block.
Order Matters: Specific Before General
Because Java picks the first matching clause, a handler for a parent type placed above a handler for its subclass would always win, and the subclass handler could never run. The compiler treats this as an error:
public class Order {
public static void main(String[] args) {
try {
Integer.parseInt("x");
} catch (Exception e) {
System.out.println("general");
} catch (NumberFormatException e) {
System.out.println("number");
}
}
}
Compiler output:
Order.java:7: error: exception NumberFormatException has already been caught
} catch (NumberFormatException e) {
^
1 error
NumberFormatException is a subclass of Exception, so the first clause already covers it. The fix is to order handlers from most specific to most general:
try {
Integer.parseInt("x");
} catch (NumberFormatException e) {
System.out.println("number"); // specific case first
} catch (Exception e) {
System.out.println("general"); // everything else
}
The order only matters between types that are related by inheritance. ArithmeticException and NumberFormatException are unrelated siblings (both extend RuntimeException through different branches), so in the Ratio example they could be listed in any order.
A useful way to remember the rule: picture the class hierarchy as a tree. Handlers must go from the leaves toward the root.
Multi-Catch: One Handler for Several Types
When different exceptions need exactly the same handling, writing the same block several times is repetitive. Since Java 7, one catch clause can list several types separated by |:
try {
// statements
} catch (TypeA | TypeB | TypeC e) {
// runs for any of the three
}
try {
int a = Integer.parseInt(parts[0]);
int b = Integer.parseInt(parts[1]);
System.out.println(a / b);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
System.out.println("Expected two whole numbers, e.g. \"10 2\"");
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero");
}
Here missing input and non-numeric input both mean "the user typed the wrong thing", so they share one message, while division by zero gets its own.
Multi-catch has three rules, each with a reason.
Rule 1: The alternatives can't be related by inheritance
import java.io.*;
public class Related {
public static void main(String[] args) {
try {
new FileReader("a.txt").close();
} catch (FileNotFoundException | IOException e) {
System.out.println(e);
}
}
}
Compiler output:
Related.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
IOException already covers FileNotFoundException, so listing both is redundant. Just write catch (IOException e).
Rule 2: The parameter is implicitly final
public class Assign {
public static void main(String[] args) {
try {
Integer.parseInt(args[0]);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
e = new NumberFormatException("replaced");
}
}
}
Compiler output:
Assign.java:6: error: multi-catch parameter e may not be assigned
e = new NumberFormatException("replaced");
^
1 error
The variable could hold either type, so allowing reassignment would make its type unclear. (A single-type catch parameter can be reassigned, although doing so is rarely a good idea.)
Rule 3: The variable's type is the closest common parent
Inside catch (NumberFormatException | ArrayIndexOutOfBoundsException e), the compiler treats e as their nearest common supertype, RuntimeException. You can call any method available on that common type (getMessage(), getClass()), but not methods that only one of the alternatives has. The next example uses this to its advantage.
Choosing Between Separate Blocks and Multi-Catch
| Situation | Use |
|---|---|
| Each exception needs a different response | Separate catch blocks |
| Several unrelated exceptions need identical handling | One multi-catch clause listing them all |
| A parent type and one of its subclasses need different handling | Separate blocks, subclass first |
| A parent type would cover everything you list | A single catch of the parent type |
Practical Example: Loading Application Settings
An application reads port and maxUsers from a .properties file at startup. Many different things can go wrong, and each deserves a precise message so whoever deploys the application can fix it quickly. If anything fails, the application falls back to safe defaults.
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
public class Settings {
record Config(int port, int maxUsers) { }
static final Config DEFAULTS = new Config(8080, 100);
static int intSetting(Properties p, String key) {
String value = p.getProperty(key);
if (value == null) {
throw new IllegalArgumentException("missing setting '" + key + "'");
}
return Integer.parseInt(value.trim());
}
static Config load(Path file) {
try {
Properties p = new Properties();
try (var reader = Files.newBufferedReader(file)) {
p.load(reader);
}
int port = intSetting(p, "port");
int maxUsers = intSetting(p, "maxUsers");
if (port < 1 || port > 65535) {
throw new IllegalArgumentException("port out of range: " + port);
}
return new Config(port, maxUsers);
} catch (NoSuchFileException | AccessDeniedException e) {
System.out.println(" file not available: " + Path.of(e.getFile()).getFileName()
+ " (" + e.getClass().getSimpleName() + ")");
} catch (IOException e) {
System.out.println(" could not read file: " + e);
} catch (NumberFormatException e) {
System.out.println(" not a whole number: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println(" invalid configuration: " + e.getMessage());
}
System.out.println(" using defaults");
return DEFAULTS;
}
public static void main(String[] args) throws IOException {
Path dir = Files.createTempDirectory("cfg");
Map<String, String> files = new LinkedHashMap<>();
files.put("good.properties", "port=9090\nmaxUsers=500\n");
files.put("text.properties", "port=ninety\nmaxUsers=500\n");
files.put("partial.properties", "port=9090\n");
files.put("range.properties", "port=70000\nmaxUsers=500\n");
for (var entry : files.entrySet()) {
Files.writeString(dir.resolve(entry.getKey()), entry.getValue());
}
List<String> names = new ArrayList<>(files.keySet());
names.add("absent.properties");
for (String name : names) {
System.out.println(name);
System.out.println(" -> " + load(dir.resolve(name)));
}
}
}
Output:
good.properties
-> Config[port=9090, maxUsers=500]
text.properties
not a whole number: For input string: "ninety"
using defaults
-> Config[port=8080, maxUsers=100]
partial.properties
invalid configuration: missing setting 'maxUsers'
using defaults
-> Config[port=8080, maxUsers=100]
range.properties
invalid configuration: port out of range: 70000
using defaults
-> Config[port=8080, maxUsers=100]
absent.properties
file not available: absent.properties (NoSuchFileException)
using defaults
-> Config[port=8080, maxUsers=100]
main creates a temporary folder with four test files and then tries to load each, plus one file that doesn't exist. The interesting part is the catch chain in load:
NoSuchFileException | AccessDeniedException— "the file can't be opened" is handled the same way whether it's missing or not readable, so a multi-catch fits. Both classes extendjava.nio.file.FileSystemException, which is the common type ofe. That's whye.getFile()compiles:getFile()is defined onFileSystemException, so it is available for either alternative (Rule 3).IOException— any other I/O failure, such as a disk error while reading. It must come after the first handler, because bothNoSuchFileExceptionandAccessDeniedExceptionareIOExceptionsubclasses. Swapping them would cause the "has already been caught" compile error.NumberFormatExceptionbeforeIllegalArgumentException— this ordering is easy to miss.NumberFormatExceptionextendsIllegalArgumentException. Listed this way,"ninety"gets the specific "not a whole number" message, while the missing key and the out-of-range port — which the code reports by throwingIllegalArgumentExceptionitself — get the "invalid configuration" message. IfIllegalArgumentExceptioncame first, the code wouldn't compile.- The successful path returns from inside the
try. For every failure, a handler prints a message, execution continues after thetrystatement, prints"using defaults"and returnsDEFAULTS. The fallback is written once instead of in every handler.
Two features from later lessons appear here briefly: throw new IllegalArgumentException(...) creates and throws an exception (the Throw Keyword lesson), and the inner try (var reader = ...) closes the file automatically (the Try With Resources lesson).
Note the design choice in intSetting: a missing key is detected with an explicit null check and reported with a clear message. The alternative — letting value.trim() throw NullPointerException and catching that — would mix real bugs with configuration problems and would produce a much less useful message.
Exceptions Thrown Inside a catch Block
The catch blocks of one statement only protect the try block. An exception thrown inside a catch block is not handled by its sibling clauses:
public class Nested {
public static void main(String[] args) {
try {
Integer.parseInt("seven");
} catch (NumberFormatException e) {
System.out.println("Handling: " + e.getMessage());
String fallback = null;
System.out.println(fallback.length());
} catch (NullPointerException e) {
System.out.println("Caught NPE");
}
System.out.println("done");
}
}
Output:
Handling: For input string: "seven"
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "fallback" is null
at Nested.main(Nested.java:8)
The NullPointerException happens while the first handler is running. The second catch (NullPointerException e) clause does not catch it, because it only applies to exceptions from the try block. The new exception propagates out of the whole statement, so "Caught NPE" and "done" are never printed.
If handler code itself can fail, it needs its own try...catch inside the catch block, or — better — the handler should be simple enough not to fail.
The helpful NPE message names the local variable fallback only when the class was compiled with debug information for local variables (javac -g). Without it, the message refers to the variable by slot number, such as "<local2>". Build tools like Maven and Gradle include this information by default.
Catching a General Type as the Last Handler
A final catch (Exception e) after specific handlers is common at application boundaries — the top of a request handler, a scheduled job or a message consumer — where one failed unit of work must not bring down the whole process:
for (Job job : jobs) {
try {
job.run();
} catch (TimeoutException e) {
retryLater(job);
} catch (Exception e) {
log.error("Job " + job.id() + " failed", e); // log with stack trace, continue with the next job
}
}
Deep inside business logic, however, a broad final handler is usually a mistake: it catches bugs such as NullPointerException together with the expected failures and makes them harder to find. Catch broadly only where your response — log and move on — is genuinely right for any failure.