Exception Handling, finally and try-with-resources
Checked versus unchecked and when each is right, how finally silently discards an exception or a return value, and suppressed exceptions in try-with-resources.
On this page
Exception questions test judgement more than syntax. Anyone can write a try/catch; the interviewer
wants to see whether you know what finally does to a return, why the exception in the log has the
wrong stack trace, and when a checked exception is a service to the caller rather than a tax on them.
Key Takeaways
Erroris not for you to catch.RuntimeExceptionis unchecked. Everything else underExceptionis checked and must be declared or handled.- A
returnor athrowinsidefinallydiscards whatever thetryblock was doing. Never put either there. - try-with-resources closes in reverse order and keeps the original exception, attaching any close failure as suppressed.
catch (Exception e)also catches everyRuntimeExceptionyou did not anticipate.catch (Throwable t)additionally catchesOutOfMemoryError, which you cannot meaningfully handle.- Wrapping without a cause —
throw new ServiceException("failed")— destroys the stack trace that would have told you why.
The hierarchy, and what it means
The compiler's rule is mechanical: a checked exception must be caught or declared. The design question underneath is not mechanical at all.
Checked says: this is a foreseeable condition the caller can reasonably do something about. A file might not exist; the caller can prompt for another path. Unchecked says: this is a programming error, or a condition nobody up the stack can recover from. A null argument, a malformed configuration value, a database that is down.
The honest test is: can the immediate caller write a catch block that does something other than log
and rethrow? If not, a checked exception only adds noise to every signature between the throw site
and the one place that actually handles it.
What finally really does
static int broken() {
try {
return 1;
} finally {
return 2; // discards the 1 — this method always returns 2
}
}
static int alsoBroken() {
try {
throw new IllegalStateException("the real problem");
} finally {
return -1; // the exception is thrown away entirely
}
}alsoBroken() returns -1 and the IllegalStateException vanishes — no log line, no stack trace,
no evidence. This is one of the nastiest bug classes in Java, because the code that hides the error
looks like defensive programming. Modern compilers and every static-analysis tool warn about it;
treat the warning as an error.
A subtler version:
static int surprising() {
int x = 1;
try {
return x; // the value 1 is captured here
} finally {
x = 99; // too late — the return value was already fixed
}
} // returns 1The return expression is evaluated before finally executes, and the result is held on the
stack. Mutating the variable afterwards changes nothing. Understanding that ordering is what the
question is really testing.
try-with-resources
Before Java 7, correctly closing two resources took a nested try/finally with null checks in every
branch, and almost nobody wrote it correctly. Now:
public void export(Path source, Path target) throws IOException {
try (BufferedReader in = Files.newBufferedReader(source);
BufferedWriter out = Files.newBufferedWriter(target)) {
String line;
while ((line = in.readLine()) != null) {
out.write(transform(line));
out.newLine();
}
}
// Both closed automatically, in reverse order: out first, then in.
// If the body throws AND close() throws, the body's exception wins and
// the close failure is attached to it as a suppressed exception.
}Three properties matter for the interview. Resources close in reverse declaration order, which is
what you want when a later resource wraps an earlier one. Anything implementing AutoCloseable
qualifies — including your own types. And the suppression behaviour solves a real problem: before
Java 7, a failure in close() would replace the exception that caused the failure, so the log showed
"connection already closed" instead of the actual error.
catch (IOException e) {
log.error("export failed", e);
for (Throwable suppressed : e.getSuppressed()) {
log.warn(" additionally, closing a resource failed", suppressed);
}
}Since Java 9 you can also use an existing effectively-final variable directly:
try (existingResource) { ... }, without redeclaring it.
Designing exceptions
public class OrderNotFoundException extends RuntimeException {
private final String orderId;
public OrderNotFoundException(String orderId) {
// The message carries the identifier. A message of "not found" is
// useless in a log with ten thousand lines an hour.
super("Order not found: " + orderId);
this.orderId = orderId;
}
public String orderId() { return orderId; }
}Four rules that hold up in review:
Always pass the cause. throw new ServiceException("payment failed", e) keeps the original stack
trace. Dropping e is the most common way teams lose the only evidence of what went wrong.
Put the identifiers in the message. An exception that says which order, which tenant, which file turns a debugging session into a single log search.
Expose the data as fields, not just text. A caller that wants to build an HTTP response should not be parsing your message string.
Do not use exceptions for control flow. Filling in a stack trace costs roughly a microsecond and
scales with depth; on a hot path throwing thousands per second is measurable. If a condition is
expected — a cache miss, a validation failure across a list — return a value. (If you genuinely need
a cheap signal, overriding fillInStackTrace() to return this removes the cost, at the price of
losing the trace.)
Anti-patterns worth naming
catch (Exception e) { } // swallowed silently
catch (Exception e) { e.printStackTrace(); } // goes to stdout, not your log aggregator
catch (Exception e) { log.error(e.getMessage()); } // message only — no stack trace
catch (Exception e) { throw new RuntimeException("error"); } // cause discardedThe third is worth calling out because it looks correct. log.error(e.getMessage()) prints one line
with no trace; log.error("could not settle batch {}", batchId, e) passes the throwable as the last
argument and logs the full trace. That distinction is a good thing to volunteer unprompted — it shows
you have debugged something at 3am.
Finally, catch (Exception e) around a block that only declares IOException still catches every
NullPointerException and IllegalStateException from your own code, converting a bug into a handled
condition. Catch the narrowest type you can actually handle.
Frequently Asked Questions
Does the finally block always run?
Why are checked exceptions considered a mistake by some?
What is a suppressed exception?
Related tutorials
- OOP Principles the Interviewer Actually ProbesThe OOP questions behind the textbook four: static vs dynamic dispatch, Liskov violations that compile cleanly, abstract class versus interface, and composition over inheritance.
- static, final and Class Initialisation OrderThe exact order the JVM initialises a class, why a static final String can survive deleting the class that declared it, effectively final, and how two classes can deadlock while loading.
- Strings: Immutability, the Pool and StringBuilderWhy String is immutable and what that buys you, how the string pool and intern() really work, why == sometimes appears to work, compact strings, and when concatenation in a loop actually costs you.
- Generics, Type Erasure and WildcardsWhat the compiler removes and what it inserts, why you cannot create an array of a generic type, PECS explained by what it enables, bridge methods, and heap pollution from unchecked varargs.