The resource leak you cannot see
Open a file, read a few bytes, then hit an exception before the line that closes it. The program recovers and carries on — but the file handle stays open. Repeat that a thousand times and the operating system runs out of descriptors, and a server that felt healthy a minute ago starts refusing every new connection.
The bug is invisible in a code review: nothing looks wrong, because the cleanup line is simply sitting there, patiently, after the statement that already threw. Manual cleanup in a finally block fixes it, but it is verbose and easy to get wrong, especially when several resources are open at once.
try-with-resources removes the footgun. Declare each resource inside the try header, and the language guarantees it is closed — whether the body returns normally or throws.
flowchart TD
O["resource opened with new"] --> U{"exception before<br/>the close line?"}
U -->|"yes"| LEAK["manual close skipped<br/>resource leaks"]
U -->|"no"| CL["close line reached"]
TW["try (Resource r = new ...)"] --> AUTO["auto-closed at end of try<br/>even when the body throws"]
style LEAK fill:#b91c1c,color:#fff
style AUTO fill:#15803d,color:#fff
Declare it in the header
The resource goes in parentheses right after try: try (Door d = new Door()) { ... }. At the end of the block the JVM calls d.close() for you, and that call happens before any catch or finally that follows.
Not every type qualifies. To be a resource, a class must implement AutoCloseable (or the older Closeable), which declares a single close() method. That is the contract the statement relies on: it can only auto-close something that knows how to close itself. The standard library's files, streams, sockets, and database connections all implement it, which is why they drop straight in.
public class Main {
// Any class implementing AutoCloseable can be a resource.
static class Channel implements AutoCloseable {
Channel() {
System.out.println("opening channel");
}
void send(String msg) {
System.out.println("sent: " + msg);
}
public void close() {
System.out.println("closing channel");
}
}
public static void main(String[] args) {
// A resource declared in the header is closed automatically at the end of the try.
try (Channel c = new Channel()) {
c.send("hello");
}
}
}
Several resources close in reverse
You can declare more than one resource, separated by semicolons: try (Door a = new Door(); Door b = new Door()) { ... }. They are opened in the order you wrote them and closed in the reverse order.
Reverse order is deliberate. If resource B was acquired after A and might depend on it, then B should be released before A — exactly how you unwind a stack of dependencies by hand. Each resource is closed independently too: if closing B throws, A is still closed afterward, so one failing cleanup cannot strand the others.
flowchart LR D1["declare A"] --> D2["declare B"] --> D3["declare C"] D3 --> BODY["try body runs"] BODY --> C1["close C"] C1 --> C2["close B"] C2 --> C3["close A"] style C1 fill:#c2410c,color:#fff style C2 fill:#c2410c,color:#fff style C3 fill:#c2410c,color:#fff
public class Main {
static class Door implements AutoCloseable {
private final String name;
Door(String name) {
this.name = name;
System.out.println("open " + name);
}
public void close() {
System.out.println("close " + name);
}
}
public static void main(String[] args) {
// Declared left to right, closed right to left (reverse order).
try (Door first = new Door("1"); Door second = new Door("2")) {
System.out.println("working");
}
}
}
A note on scope
A resource declared in the header is scoped to the try block: after the closing brace, the variable is no longer usable. The compiler also treats it as effectively final, so you cannot reassign it inside the body — that would make its lifetime ambiguous, and the auto-close would not know which object to close.
That constraint is a feature. It guarantees that exactly the object you opened is the one that gets closed, once, in a predictable place. If you need a fresh resource partway through, open a nested try-with-resources for it rather than rebinding the outer variable.
Cleanup runs even when the body throws
The guarantee is not just for the happy path. If the try body throws an exception, the resources are still closed — the auto-close happens as part of unwinding the block, before control leaves for a matching catch. That is the whole point: you no longer have to remember to close things in every branch, because there are no branches to remember.
Contrast that with the old try/finally idiom, where a careless reorder or a forgotten finally quietly leaks the resource. The compiler now tracks the resource for you, which is the kind of tedious bookkeeping machines are good at and humans are not.
flowchart TD
BODY["try body throws E"] --> CLOSE{"close() also throws?"}
CLOSE -->|"yes, throws T"| SUP["T is suppressed<br/>attached to E"]
SUP --> PROP["E propagates, carrying T"]
CLOSE -->|"no"| PROP2["E propagates alone"]
style SUP fill:#b45309,color:#fff
What if close itself fails?
Cleanup can fail too — a buffer flush may throw, or a socket may already be broken. If the try body threw an exception E and close() then throws T, Java does something careful: T is not lost, and it does not replace E. Instead T is attached to E as a suppressed exception, and E continues to propagate as the primary failure.
You can read those attachments back with e.getSuppressed(), which returns an array of the close-time exceptions chained onto the main one. Nothing is silently dropped. If the body succeeded and only close() throws, then that exception propagates normally — there is simply no primary exception to attach it to.
What does this print? Two resources are declared, then the body runs without throwing. Type the exact output, six lines.
public class Main {
static class Door implements AutoCloseable {
String name;
Door(String name) {
this.name = name;
System.out.println("open " + name);
}
public void close() {
System.out.println("close " + name);
}
}
public static void main(String[] args) {
try (Door a = new Door("A"); Door b = new Door("B")) {
System.out.println("using doors");
}
System.out.println("after");
}
}
The constructors run in declaration order: open A, then open B.
The body prints its line, then the try ends with no exception thrown.
Resources close in reverse order (close B, then close A), before the line after the block.
In try (Door a = new Door(); Door b = new Door()) { ... }, in what order are the doors closed?
Resources close in the reverse of the order they were declared — last opened, first closed. Declare b second and it closes first, then a, mirroring how you unwind dependencies by hand.
What does this print? The body throws, but the resources still close. Predict the exact output, six lines.
public class Main {
static class Door implements AutoCloseable {
String name;
Door(String name) {
this.name = name;
System.out.println("open " + name);
}
public void close() {
System.out.println("close " + name);
}
}
public static void main(String[] args) {
try (Door a = new Door("A"); Door b = new Door("B")) {
System.out.println("boom coming");
throw new RuntimeException("body failed");
} catch (RuntimeException e) {
System.out.println("caught: " + e.getMessage());
}
}
}
The throw happens inside the body, after both doors are open.
Resources are still closed — in reverse order — before the catch is chosen.
Only after close B and close A does the matching catch print its line.
What does this print? Here close() throws too. Predict the exact output, five lines.
public class Main {
static class Door implements AutoCloseable {
String name;
Door(String name) {
this.name = name;
System.out.println("open " + name);
}
public void close() {
System.out.println("close " + name);
throw new RuntimeException("close failed " + name);
}
}
public static void main(String[] args) {
try (Door a = new Door("A")) {
System.out.println("body throws");
throw new RuntimeException("body failed");
} catch (RuntimeException e) {
System.out.println("caught: " + e.getMessage());
System.out.println("suppressed: " + e.getSuppressed().length);
}
}
}
The body throws first, so it becomes the primary exception E.
close() runs and also throws — but since E is already in flight, the close exception is attached to E, not propagated.
getMessage()shows the body message;getSuppressed().lengthcounts the close exception that was attached.
Which resource is guaranteed to be closed automatically by try-with-resources?
Automatic closing applies only to resources declared in the try header. A variable declared inside the body is an ordinary local — not tracked, and its close() is never called for you. The header declaration is what enrols a resource in cleanup.
Recap
- try-with-resources declares resources in the
tryheader and closes them automatically, with nofinallyto remember. - A resource must implement
AutoCloseable(orCloseable); files, streams, sockets, and connections all do. - Multiple resources open in declaration order and close in reverse order, each independently.
- Cleanup runs even when the body throws, before control reaches a catch.
- If both the body and
close()throw, the close exception is suppressed onto the primary one and is still readable viagetSuppressed().
That closes the module on handling failure. Next you will turn to moving bytes in and out of files with java.nio.file.