Skip to content

Java · Exceptions

Java Try-With-Resources — Automatic Resource Management Explained

8 min read Updated 2026-06-20 Share:

Practice try-with-resources interview questions

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.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel