Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Beginner6 min readUpdated
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

  • Error is not for you to catch. RuntimeException is unchecked. Everything else under Exception is checked and must be declared or handled.
  • A return or a throw inside finally discards whatever the try block 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 every RuntimeException you did not anticipate. catch (Throwable t) additionally catches OutOfMemoryError, 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

Everything under Error, and everything under RuntimeException, is unchecked. The rest of Exception is checked.

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

ReturnFromFinally.java
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:

the value is captured before finally runs
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 1

The 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:

ReportExporter.java
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.

reading suppressed exceptions
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

OrderNotFoundException.java
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

four ways to lose an error
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 discarded

The 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?
Almost always. It is skipped only if the JVM exits during the try block via System.exit(), if the thread is killed at the OS level, or if the JVM crashes. An infinite loop or a blocked call inside try also means it never gets there. For every ordinary path — normal completion, a return, a break, or a thrown exception — finally runs.
Why are checked exceptions considered a mistake by some?
Because they do not compose. A checked exception in a lambda cannot propagate through a Stream or a functional interface, so it gets wrapped or swallowed. They also force every intermediate caller to declare or handle something it cannot act on, which produces throws clauses that leak implementation details up the stack. Modern APIs — including most of Spring — favour unchecked exceptions for this reason.
What is a suppressed exception?
In try-with-resources, if the body throws and then closing the resource also throws, the close() exception would normally replace the original and hide the real cause. Java instead attaches it to the primary exception, retrievable with getSuppressed(), and prints it in the stack trace under "Suppressed:". The exception you actually care about is preserved.

Related tutorials