Java try...catch
Handle runtime errors in Java with try...catch blocks to keep your program running when something goes wrong.
try/catch is the smallest unit of exception handling in Java. You wrap the risky code in try, and follow it with one or more catch blocks that say what to do if a particular kind of exception is thrown. Together they form a single statement; you can't have one without the other (or without a finally, which we'll see in a later chapter).
Anatomy
try {
// code that might throw
} catch (ExceptionType e) {
// code that runs only if a matching exception was thrown
}A few things are easy to miss at first glance:
- The variable in the
catch(eby convention) is scoped to the catch block. You can name it whatever you want;eandexare the common choices. - The catch parameter is effectively final — you can read it but you shouldn't reassign it. Treating it as immutable keeps the code easy to follow.
- The
tryblock is its own scope. Variables declared inside it are not visible to thecatchor after the statement. If you need a value computed in thetrylater, declare it outside.
String content;
try {
content = Files.readString(path);
} catch (IOException e) {
content = ""; // works because `content` was declared outside the try
}
System.out.println(content);Which catches match
A catch (T e) block runs when the thrown exception is an instance of T — including any subtype of T. So:
catch (Exception e)catches almost everything:IOException,NullPointerException, your own custom exceptions.catch (RuntimeException e)catches runtime bugs but not checked exceptions likeIOException.catch (NullPointerException e)catches only NPEs (and subclasses, of which there are typically none).
Inside the try, only the first matching catch runs. If you list a broader type before a narrower one, the narrower catch is unreachable and the compiler refuses:
try { ... }
catch (Exception e) { ... } // matches everything
catch (IOException e) { ... } // ERROR: unreachableAlways list catches from most specific to most general.
What to do in a catch
A catch block isn't a place to make exceptions go away. It's a place to decide what to do about a known failure. The realistic options are:
- Recover — retry, fall back to a default, switch to a different resource. This is the best case, and it's rarer than you'd think.
- Log and rethrow — record details and let the exception keep propagating to a higher-level handler that knows what to do.
- Wrap and rethrow — translate a low-level exception into one that fits this layer's vocabulary (
IOException→ConfigLoadException). - Translate to a return value — when the failure is genuinely expected (e.g. parsing user input), return
Optional.empty()or a sentinel.
The thing not to do is catch an exception and quietly continue without logging or acting on it. That's how bugs disappear into thin air. We'll come back to this in the best-practices chapter.
Reading exception information
The catch parameter is an object. Three methods you'll use constantly:
e.getMessage()— the human-readable description. May benull.e.toString()— class name plus message, e.g.java.io.IOException: file not found.e.printStackTrace()— writes the trace toSystem.err. Convenient during debugging, but use a real logger in production code (it routes the trace through the same channel as everything else).
A fourth, e.getCause(), returns the underlying exception when one was wrapped — useful when you need to understand the original failure inside a translation layer.
Catching Exception vs. catching Throwable
You can write catch (Throwable t) and it will catch everything — including Errors like OutOfMemoryError. Don't. Errors mean the JVM is in trouble and your code is in no shape to react sensibly to them. Catching Throwable masks bugs that should crash the process.
Stick to Exception or one of its subtypes. If you genuinely need to log the unexpected before letting it die, the right shape is catch (RuntimeException e) { log; throw; }.
A worked example
A small utility that parses an integer from each line of input. Some lines are valid numbers, some aren't, one is null. We use try/catch to keep going past the bad lines and tally what happened. Calling line.trim() throws a NullPointerException on the null entry, while Integer.parseInt throws a NumberFormatException on anything that isn't an integer — two distinct exception types, each with its own catch.
The output is:
not an integer: "hello"
null line — skipped
not an integer: "3.14"
---
parsed: 3, failed: 3, sum: 118
Two things to notice. First, the program finishes. Without the catches, the first bad line would terminate the run and nothing past it would be counted. Second, the two failures are distinguished: the null line takes the NullPointerException branch and prints its own message, while "hello" and "3.14" take the NumberFormatException branch. Selecting catches by type is how you keep different failures from blurring together.
What's next
One catch is the simplest case. Real code usually has to handle several different failures at once. Continue to Java multiple catch blocks and multi-catch. After that, the finally block and try-with-resources round out the syntax, while throw and throws cover the other side: signalling failures from your own code.