Interview Prep Zoneby CuriouserLabs

Question 4 of 5 · senior level

What are the classic pitfalls with finally — returns, thrown exceptions, and lost causes?

🎤 Say this first

Two rules: never `return` and never throw from `finally`. A return in finally silently discards both the try's return value and any in-flight exception; an exception in finally replaces the original one (the true cause vanishes). finally is for cleanup only — and since Java 7, try-with-resources handles the resource case better by preserving the primary exception and suppressing close failures.

The full picture

Three traps in one screen
int f() {
    try { return 1; }
    finally { return 2; }        // returns 2 — the 1 is discarded
}

int g() {
    try { throw new IllegalStateException("REAL cause"); }
    finally { return -1; }       // exception SWALLOWED — g() just returns -1
}

void h() throws IOException {
    InputStream in = open();
    try { read(in); }            // throws ReadException…
    finally { in.close(); }      // …close() throws too -> ReadException LOST
}                                // try-with-resources keeps it + suppresses close()

// Also: System.exit() in try skips finally; finally does run on return/exception,
// but NOT on JVM kill (kill -9) — never put durability logic in finally.
  • javac even warns on finally blocks that can't complete normally — treat that warning as an error.
  • Cleanup that itself can fail (flush/close) belongs in try-with-resources; finally is for the truly non-throwing tidy-up (unlock, MDC clear, timer stop).
  • lock.unlock() in finally is the canonical good use — and pairs with the rule that the lock() call sits before the try.

🔄 Likely follow-up questions

  • Why must lock.lock() be placed before the try, not inside it?
  • What does the compiler generate for try-finally (why did older JVMs duplicate bytecode)?
  • How do suppressed exceptions appear in logs and how do you surface them?