The leak you keep writing
Every file, socket, and database connection you open holds an operating-system handle
that won't free itself. Before Java 7 the only way to release it reliably was a finally
block — and that block was a magnet for bugs. You forgot it, you null-checked it wrong, or
worst of all, a failure in close() quietly swallowed the real exception your code threw.
Try-with-resources (Java 7) turns that whole ritual into a few characters of syntax and,
critically, gets the exception handling more correct than most hand-written cleanup ever
did. This guide is about how it works, not just how to type it.
The manual-finally problem
Here is the pattern try-with-resources replaces. The resource has to be declared outside
the try so finally can see it, which forces a null initializer and a null check,
because the constructor on line two could itself throw before the assignment completes.
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("a.txt"));
return br.readLine();
} finally {
if (br != null) br.close(); // close() can ALSO throw IOException
}
That last comment is the trap. If readLine() throws and close() throws, the
exception from close() propagates and the original — the one describing what actually
went wrong — is lost forever. Add a second resource and you nest two try/finally
blocks to guarantee the outer one closes even when the inner constructor fails. The
boilerplate grows faster than the logic it guards.
AutoCloseable: the one method that opts you in
Any object can become a managed resource by implementing AutoCloseable, a
single-method interface added to java.lang in Java 7. Only types that implement it (or
its subtype) are legal inside the try parentheses; anything else is a compile error.
public interface AutoCloseable {
void close() throws Exception; // broadest checked exception allowed
}
close() is declared to throw Exception because AutoCloseable is the general
abstraction — a JNI handle or a DB connection should be free to report any failure. An
implementation is always allowed to narrow that: override it as throws IOException,
or, better still, throws nothing at all. A close() that cannot fail is dramatically
easier to reason about, so prefer narrowing whenever your cleanup genuinely can't error.
Closeable, and why it predates the feature
Closeable (Java 5, in java.io) existed years before try-with-resources, built for I/O
streams. Java 7 retrofitted it to extend AutoCloseable, so every existing
InputStream, Reader, and Writer instantly became a valid resource with no code
changes — a quietly brilliant bit of backward compatibility.
public interface Closeable extends AutoCloseable {
void close() throws IOException; // narrowed; and MUST be idempotent
}
Two contract differences matter. Closeable.close() narrows the throw to IOException,
and it must be idempotent — calling it twice has no extra effect and never throws.
That second rule exists because a stream can be closed both explicitly by your code and
again automatically by the try statement; the redundant call must be harmless. Reach for
Closeable when your resource is I/O-flavoured, AutoCloseable for everything else.
What the compiler actually generates
Try-with-resources is syntactic sugar. The compiler desugars it into an ordinary
try/finally with exception bookkeeping baked in. Seeing the expansion explains every
behaviour that follows.
// try (Resource r = ...) { body } becomes roughly:
Resource r = ...;
Throwable primary = null;
try {
body;
} catch (Throwable t) {
primary = t;
throw t;
} finally {
if (r != null) { // null-safe close
if (primary != null) {
try { r.close(); }
catch (Throwable sup) { primary.addSuppressed(sup); } // attach, don't replace
} else {
r.close(); // body was clean; let close throw freely
}
}
}
The close still lives in a finally, but it is a smart one. When the body has already
failed, a failure from close() is attached to the original rather than thrown over
it. That single addSuppressed call is the whole reason try-with-resources is safer than
anything you'd write by hand.
Reverse close order and multiple resources
Declare several resources separated by semicolons and each is closed independently. They
close in the reverse order of declaration — last opened, first closed — which is
exactly what dependency stacking requires: a BufferedWriter wrapping a FileWriter must
flush and close before the FileWriter it sits on top of.
try (FileInputStream in = new FileInputStream("src.txt");
FileOutputStream out = new FileOutputStream("dst.txt")) {
in.transferTo(out);
} // close order: out first, then in
Each close is guarded on its own. If closing out throws, the runtime still closes
in — there is no path where a failing close abandons the resources beneath it. The same
guarantee covers construction: resources initialize left to right, and if a later
constructor throws, every resource already opened is closed in reverse before the exception
escapes. Nothing leaks, even half-built.
Suppressed exceptions and the lost-exception bug
This is the conceptual core. When the body throws and a close() also throws,
try-with-resources propagates the body's exception — the useful one — and tucks the
close() exception onto it as a suppressed exception, reachable via
Throwable.getSuppressed().
try (var r = new FlakyResource()) {
throw new RuntimeException("body failed"); // becomes the PRIMARY
} // r.close() also throws -> attached as suppressed, not lost
catch (Exception e) {
for (Throwable s : e.getSuppressed())
System.out.println("suppressed: " + s); // the close() failure
}
Compare that to the hand-written finally, where an exception from the finally block
replaces the body's — the genuine cause vanishes and you debug the cleanup failure
instead of the real one. Try-with-resources inverts the priority: body wins, cleanup rides
along. The stack trace even prints the extras under a Suppressed: heading. Note the flip
side: if the body succeeds and only close() throws, there's no primary to attach to, so
that exception propagates normally — a failed final flush is a real error, not a footnote.
The Java 9 effectively-final form
Originally you had to declare the resource inside the parentheses, leading to clumsy
re-declarations like try (Reader r2 = r1). Java 9 lets you name an already-declared
variable, provided it is final or effectively final (never reassigned after
initialization).
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
try (br) { // Java 9+: reference, don't redeclare
return br.readLine();
} // br is still closed automatically before the method returns
The variable must be stable so the runtime closes the exact object it managed; reassign it
and the compiler rejects the code. Every exit path — fall-through, early return,
break, or a thrown exception — runs the generated close first, so the resource is gone
before control actually leaves the block.
Writing your own AutoCloseable
The feature isn't only for files. Anything with a clear "enter, then guaranteed exit" shape
fits — timers, locks, transactions, logging context. Implement close(), make it
idempotent with a guard flag, and avoid throwing from it where you can.
class Timer implements AutoCloseable {
private final long start = System.nanoTime();
private boolean closed = false;
@Override public void close() { // narrowed: throws nothing
if (closed) return; // idempotent guard
closed = true;
System.out.println("took " + (System.nanoTime() - start) + "ns");
}
}
try (var t = new Timer()) { doWork(); } // elapsed time printed on exit
If you append catch/finally clauses to a try-with-resources, remember the ordering:
resources close first, then catch and finally run. So a catch block sees an
already-closed resource and can even handle exceptions thrown by close() itself.
When try/finally is still the right tool
Try-with-resources only manages objects that implement AutoCloseable. For cleanup that
isn't a closeable resource — resetting a flag, restoring thread-local state — plain
try/finally remains correct. The canonical example is locking. A Lock is not
AutoCloseable, and the acquisition must happen before the try, or a failed acquire
would wrongly trigger an unlock.
lock.lock(); // acquire BEFORE the try
try {
criticalSection();
} finally {
lock.unlock(); // try/finally, NOT try-with-resources
}
You can wrap a lock in a tiny AutoCloseable that unlocks in close(), but you must
still acquire outside the block. "When is try-with-resources the wrong tool?" is a favourite
interview question, and this lock idiom is the answer.
Recap
Try-with-resources replaces the error-prone manual finally with a declaration that
closes any AutoCloseable automatically on every exit path. Under the hood it desugars
to a try/finally that null-checks the resource and, crucially, records a close() failure
as a suppressed exception instead of letting it erase the body's — fixing the
lost-exception bug that plagued hand-written cleanup. Resources close in reverse order,
even when a later constructor throws; Closeable extends AutoCloseable and demands an
idempotent close; Java 9 added the effectively-final form. Write your own resources for
any scoped concern, and keep try/finally for non-closeable teardown like lock release.