Topic 7.2
try, catch and finally
In one line
try marks code that might fail, each catch handles one kind of exception, and finally holds clean-up code that runs whether the try finished normally, returned early, or threw.
Think of it like this
Cooking with an oven. You try to bake a cake. If it burns, you catch that problem and order one from a bakery instead. If the oven door jams, that's a different problem with a different plan. And finally, whatever happened, you switch the oven off. You never leave it on just because the cake burned. Java's try/catch/finally has exactly this shape.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- `try` block
- The block of code you want to protect. If something in it throws, the matching handler runs.
- `catch` block
- A handler for one type of exception (and its subtypes). It receives the exception object in a variable, usually named
e. - `finally` block
- Code that runs after the
try(and anycatch) no matter how they ended. Used for clean-up. - Handler
- Another name for a catch block: the code that handles an exception.
- Clean-up
- Giving back something you borrowed from the system, such as closing a file, a network connection or a lock.
- Swallowing an exception
- Catching an exception and doing nothing useful with it, so the failure disappears without a trace.
- Suppressed exception
- A second exception that happened while cleaning up after a first one. It's attached to the first so it isn't lost (Topic 7.6).
- Abrupt completion
- The JLS term for a block ending early because of
return,break,continueor a thrown exception, rather than running off its last line.
Step by step
01The shape of the statement
A full try statement has one try block, zero or more catch blocks and an optional finally. At least one catch or the finally is required.
Think of the try as the plan, each catch as a backup plan for one kind of failure, and finally as the 'switch the oven off' step.
try {
int n = Integer.parseInt(input); // may throw NumberFormatException
System.out.println(100 / n); // may throw ArithmeticException
} catch (NumberFormatException e) {
System.out.println("not a number: " + input);
} catch (ArithmeticException e) {
System.out.println("can't divide by zero");
} finally {
System.out.println("done with " + input);
}02What runs, in which order
Three paths are possible. No exception: the whole try, then finally, then the code after. A matching exception: the try up to the failing line, then the matching catch, then finally, then the code after. A non-matching exception: the try up to the failing line, then finally, then the exception leaves the method.
The flowchart below is worth memorising. Every path goes through finally.
03Order catch blocks from specific to general
NumberFormatException extends IllegalArgumentException, which extends RuntimeException, which extends Exception. A catch for a parent type catches all its children.
So put the narrowest type first. If you write the broad one first, the narrow one is unreachable and javac stops you. This is a compile error, not a warning: Java refuses to keep dead handlers around.
04finally runs even on return
If the try executes return, the return value is computed and stored in a hidden local variable, then finally runs, then the method returns the stored value. So changing a variable in finally doesn't change an already-computed primitive result.
For an object, the stored value is the reference. finally can't swap the object, but it can still change the object's contents (append to a StringBuilder, add to a list), and the caller sees that change. Same pass-by-value rule as Topic 3.x.
static int count() {
int x = 1;
try {
return x; // 1 is saved in a hidden slot
} finally {
x = 99; // too late: the saved 1 is returned
}
}05Never return or throw from finally
A return in finally overrides whatever the try or catch was doing: a pending return value is replaced, and a pending exception is silently discarded. javac only warns about it with -Xlint:finally (finally clause cannot complete normally), so most builds won't even mention it.
An exception thrown from finally (perhaps by a close() call) also replaces the original exception. Your logs show the clean-up failure and the real problem is gone. Wrap risky clean-up in its own try/catch, or better, use try-with-resources.
06How javac compiles finally
The JVM has no 'finally' instruction. javac copies the finally block's code onto every exit path: after the normal end of the try, at the end of each catch, and before each return, break or continue that leaves the block. It also adds a catch-all handler (type any in the exception table) that stores the exception, runs another copy of the finally code, and rethrows it.
That's why a large finally block can noticeably grow a method's bytecode. Very old compilers used jsr/ret subroutine instructions instead; the modern class-file verifier (Java 7+ class files) no longer allows them.
07Scope: declare before the try what finally needs
A variable declared inside try { ... } dies at its closing brace. If finally must close a reader, declare it as Reader r = null; before the try, assign it inside, and check for null in finally. That pattern is verbose and easy to get wrong, which is exactly why Topic 7.6 replaces it.
Try it yourself
- 1
Add a path
In the first example, call
attempt(null). Predict whatInteger.parseInt(null)throws (hint: it's aNumberFormatException, with a message likeCannot parse null string; the exact wording varies between JDK versions). Predict all lines, then run. - 2
Remove a catch
Delete the
ArithmeticExceptioncatch from the first example and callattempt("0")last. Predict: doesfinallystill print? Doesafter the try statementprint? Then run and read the stack trace. - 3
Change the returned object
In
appendsAfterReturn, replaceb.append("b")withb = new StringBuilder("zzz"). Predict the last line before running. Why is it different from the append version?
Code & diagrams
Expected output
-- attempt("4")
try: start
try: parsed 4
try: share 25
finally: always runs
after the try statement
-- attempt("abc")
try: start
catch NFE: For input string: "abc"
finally: always runs
after the try statement
-- attempt("0")
try: start
try: parsed 0
catch AE: / by zero
finally: always runs
after the try statementThe first line prints before 'returned 1' because the method call runs while the string is being built.
Expected output
finally set value to 99
returned 1, value is now 99
returned 2
finally returned normally
object returned: abThe careful version is what try-with-resources generates for you automatically (Topic 7.6).
Expected output
careless: java.lang.IllegalArgumentException: cleanup failed
suppressed count: 0
careful: java.lang.IllegalStateException: the real problem
suppressed: java.lang.IllegalArgumentException: cleanup failedCorrect but noisy. Topic 7.6 shrinks this to three lines.
BufferedReader reader = null; // declared outside so finally can see it
try {
reader = new BufferedReader(new FileReader("notes.txt"));
System.out.println(reader.readLine());
} catch (IOException e) {
System.out.println("could not read: " + e.getMessage());
} finally {
if (reader != null) { // the constructor may have failed
try {
reader.close();
} catch (IOException e) {
// close failed: log it, but don't hide the first error
}
}
}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
Put the general catch first
Write catch (Exception e) { ... } catch (RuntimeException e) { ... } after a try block.
Break #2
A try with nothing after it
Write try { System.out.println("x"); } with no catch and no finally.
Break #3
Catch a checked exception that can't happen
Write try { System.out.println("hi"); } catch (IOException e) { ... }.
Myth vs fact
Myth
finally always runs, no exceptions.
Fact
It runs on every normal and abrupt exit from the try, but not if the JVM stops: System.exit(), a crash, kill -9, or a try that never finishes (infinite loop, deadlock). A daemon thread's finally also doesn't run if the JVM exits around it.
Myth
After a catch, execution goes back into the try.
Fact
The rest of the try is abandoned for good. Execution continues after the entire try statement. If you need a retry, write a loop around the try.
Myth
Changing a variable in finally changes what the method returns.
Fact
The return value was already evaluated and saved. For primitives and reassigned references nothing changes; only mutations to the returned object are visible.
Myth
Several catch blocks can run for one exception.
Fact
Exactly one catch runs: the first whose type matches. If you need the same handling for several types, use a multi-catch (Topic 7.7).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
JLS 14.20.2 defines the rules precisely: if the
trycompletes abruptly for reason R andfinallycompletes abruptly for reason S, the whole statement completes abruptly for reason S and R is discarded. That one sentence is behind both the 'return in finally' and the 'exception in finally' traps. - ▸
javac inlines the finally body once per exit path plus once in the
anyhandler. A finally block containing a big switch or many statements, in a try with many returns, can push a method past HotSpot's 8000-byteHugeMethodLimit; withDontCompileHugeMethodson (the default) such a method is never JIT-compiled and stays interpreted, quietly costing performance. Extract large clean-up into a method. - ▸
Catching
ThrowableorErrorin general code is dangerous:OutOfMemoryErrorcan strike anywhere,StackOverflowErrorleaves the thread in a fragile state, andThreadDeath(deprecated) and the runtime's internal errors expect to propagate. Framework top-level loops do it to log and keep a worker alive; application code almost never should. - ▸
The JIT treats exception paths as cold. A try/catch whose catch path is actually hot (thrown thousands of times a second) may run in a deoptimised state, and implicit exceptions in hot code may lose their stack traces (Topic 7.1's
OmitStackTraceInFastThrow).
Remember this
- 1
A
tryblock contains the statements that might throw. It must be followed by at least onecatch, afinally, or both (or, from Java 7, it can declare resources: Topic 7.6). Eachcatch (SomeType e)names an exception type and a variable. When something is thrown inside thetry, the rest of thetryis skipped and the JVM tests the catch blocks in order, top to bottom, choosing the first one whose type matches (the thrown object is aninstanceofthat type). - 2
Because the first match wins, a catch for a subclass must come before a catch for its superclass. If
catch (Exception e)came first, a latercatch (NumberFormatException e)could never run, and javac rejects it withexception NumberFormatException has already been caught. The compiler also rejects a catch for a checked exception that thetrybody can't throw (is never thrown in body of corresponding try statement). - 3
After a catch block finishes, execution continues after the whole try statement. It never returns into the
try. If no catch matches, the exception keeps propagating to the caller, exactly as if there were no try at all (butfinallystill runs on the way out). - 4
finallyruns in every way thetrycan end: normal completion, an exception that's caught, an exception that isn't caught,return,breakandcontinue. That makes it the place for clean-up: closing files, releasing locks, resetting a flag. The only ways to skip it are drastic:System.exit(), the JVM crashing or being killed, or thetrynever finishing (an infinite loop or a deadlock). - 5
finallyhas two traps. First, areturninsidefinallyreplaces the pending return value and even swallows a pending exception, so never return fromfinally. Second, iffinallythrows, its new exception replaces the original one, which is lost.try-with-resources(Topic 7.6) was added in Java 7 largely to fix that second problem, by recording the clean-up exception as a suppressed exception instead. - 6
Variables declared inside the
tryblock are local to it: thecatchandfinallyblocks can't see them. If you need a value infinally, declare the variable before thetry. The catch parametereis local to its own catch block, so every catch can reuse the namee.
Explain it without notes
Walk through every way a try/catch/finally statement can be executed.
Why must catch blocks for subclasses come before catch blocks for superclasses?
What happens when try returns a value and finally also returns or modifies it?
When does finally not run?
Why is throwing from finally dangerous, and what's the fix?
Practice
Write static int safeParse(String s, int fallback) using try/catch, returning the fallback for bad input. Test with "42", "4x" and null.
Write a loop that tries to divide 60 by each of {3, 0, 5}, printing the result or skip 0, and prints loop done once at the end. Use finally to print checked <n> after each attempt.
Predict, then verify: a method with try { throw new RuntimeException("A"); } catch (RuntimeException e) { throw new RuntimeException("B"); } finally { System.out.println("F"); }. Catch in main and print the message.
Trade-offs
- ↔
One big try vs several small ones: one try around a whole method reads cleanly but hides which line failed and handles everything the same way; small trys around the risky calls give precise handling at the cost of more nesting.
- ↔
Catch here vs let it propagate: catching close to the failure lets you add context or recover; letting it propagate keeps low-level code simple and lets the layer that knows the business rule decide. Catch only where you can do something meaningful.
- ↔
Manual finally vs try-with-resources: finally works for any clean-up (resetting state, unlocking), but closing resources by hand is error-prone; for anything
AutoCloseable, try-with-resources is shorter and handles suppressed exceptions correctly.
Done when you can
Done when you can trace the exact output of any try/catch/finally for normal, caught and uncaught cases.
Done when you order catch blocks from specific to general without thinking.
Done when you can explain why
returninfinallyis a bug.Done when you can list the situations where
finallydoes not run.Done when you can protect a primary exception from being replaced by a clean-up failure.