Ch. 7 · Java

Java Try-With-Resources and Cleanup

How try-with-resources closes resources in reverse order, keeps the primary exception with suppressed close failures, and beats finally.

~6 min readintermediateupdated Oct 6, 2026

“Why is try-with-resources better than closing in finally?” is a common question in Java interviews, along with “What happens if both the body and close throw?” and “In what order are resources closed?” The answers reveal whether you understand exception masking, suppressed exceptions and resource lifecycles, all of which matter when a service leaks file handles or database connections.

Resource leaks are a classic production failure. A connection pool exhausts, a process hits its open-file limit, or a lock stays held. try-with-resources exists to make the correct cleanup pattern the easy one.

Before you start

You should know try, catch and finally, and the difference between checked and unchecked exceptions. Understand that resources such as files, sockets, JDBC connections and locks are held by the operating system or a pool, not just the heap, so the garbage collector cannot be relied on to release them promptly.

The short answer

try-with-resources declares one or more AutoCloseable resources in the try header and guarantees that close() is called on each when the block exits, normally or by an exception. Resources are closed in the reverse order of declaration, and closing happens before any catch or finally clause runs. If the body throws and close() also throws, the body’s exception is propagated and the close failure is attached to it with addSuppressed, so nothing is lost. A hand-written finally gets this wrong by default, because an exception from close replaces the original.

How it works

The compiler expands this:

try (BufferedReader in = Files.newBufferedReader(path)) {
    return in.readLine();
}
java

into roughly this:

BufferedReader in = Files.newBufferedReader(path);
Throwable primary = null;
try {
    return in.readLine();
} catch (Throwable t) {
    primary = t;
    throw t;
} finally {
    if (in != null) {
        if (primary != null) {
            try {
                in.close();
            } catch (Throwable closeFailure) {
                primary.addSuppressed(closeFailure);
            }
        } else {
            in.close();
        }
    }
}
java

Notice three details. If the resource initializer itself throws, there is nothing to close and the exception propagates directly. A null resource is allowed and simply skipped. And if only close() throws, its exception is the one propagated.

AutoCloseable.close() may throw Exception; the narrower java.io.Closeable.close() throws IOException and is required to be idempotent, so calling it twice has no effect. Implementations of AutoCloseable are encouraged, but not required, to be idempotent too.

Compare the pre-Java 7 version that many codebases still contain:

BufferedReader in = null;
try {
    in = Files.newBufferedReader(path);
    return in.readLine();          // suppose this throws "disk read error"
} finally {
    if (in != null) {
        in.close();                // suppose this throws too: it replaces the read error
    }
}
java

The original failure disappears from the logs, and you debug the wrong problem.

Step-by-step walkthrough

Step 1: Declare each resource in the header

try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement("SELECT name FROM users WHERE id = ?")) {
    ps.setLong(1, id);
    try (ResultSet rs = ps.executeQuery()) {
        return rs.next() ? rs.getString(1) : null;
    }
}
java

Declare each closeable object as its own resource. Wrapping in one expression, like new BufferedReader(new FileReader(file)), means that if the outer constructor throws, the inner reader is never closed.

Step 2: Know the closing order

Resources close in reverse order: the ResultSet, then the PreparedStatement, then the Connection. That matches dependencies, since later resources usually depend on earlier ones.

Step 3: Inspect suppressed exceptions when debugging

try {
    processFile(path);
} catch (IOException e) {
    log.error("Processing failed", e);           // stack trace prints "Suppressed:" sections
    for (Throwable s : e.getSuppressed()) {
        metrics.increment("close.failures");
    }
}
java

Logging frameworks print suppressed exceptions automatically as part of the stack trace.

Step 4: Make your own types AutoCloseable

public final class TimedSection implements AutoCloseable {
    private final String name;
    private final long start = System.nanoTime();

    public TimedSection(String name) { this.name = name; }

    @Override
    public void close() {                        // no checked exception: callers need no catch
        long ms = (System.nanoTime() - start) / 1_000_000;
        System.out.println(name + " took " + ms + " ms");
    }
}

try (var timer = new TimedSection("import")) {
    runImport();
}
java

Narrow the throws clause of close() to what it can really throw, ideally nothing, so callers are not forced to catch Exception.

Step 5: Reuse existing variables on Java 9+

BufferedWriter out = Files.newBufferedWriter(target);
try (out) {                                      // Java 9+: effectively final variable
    out.write(report);
}
java

The variable must be final or effectively final.

Worked scenario

A batch exporter streams rows into a CSV file and occasionally fails with “Too many open files” after running for hours. The code:

for (Path input : inputs) {
    Stream<String> lines = Files.lines(input);
    lines.map(this::transform).forEach(writer::println);
}
java

Files.lines opens a file handle that is released only when the stream is closed, and nothing closes it. Collection-backed streams hold no resources, so developers forget that I/O-backed streams do. After a few thousand inputs, the process exhausts its file descriptors.

A second bug lurked in the writer cleanup:

finally {
    writer.close();      // if this threw, the export's real exception vanished
}
java

The fix uses try-with-resources for both:

try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(target))) {
    for (Path input : inputs) {
        try (Stream<String> lines = Files.lines(input)) {
            lines.map(this::transform).forEach(writer::println);
        }
    }
    if (writer.checkError()) {
        throw new IOException("Failed writing " + target);
    }
}
java

Each input’s handle is released per iteration, and any failure while closing the writer is attached to the primary exception. Note that PrintWriter swallows I/O errors, so the explicit checkError() call is needed to surface them.

Common mistake

  • Nesting constructors in one resource. If the outer constructor throws, the inner resource leaks; declare each separately.
  • Thinking catch runs before close. Resources are already closed by the time catch or finally executes, so you cannot use them there.
  • Forgetting I/O-backed streams. Files.lines, Files.list and Files.walk must be closed.
  • Closing a borrowed resource. If a method receives a connection from its caller, the caller owns it; closing it inside the method breaks the caller.
  • Declaring close() throws Exception. It forces every user into a broad catch.

Verify the behavior

import static org.junit.jupiter.api.Assertions.*;
import java.util.*;
import org.junit.jupiter.api.Test;

class TwrTest {
    record Res(String name, List<String> log, boolean failOnClose) implements AutoCloseable {
        @Override public void close() {
            log.add("close " + name);
            if (failOnClose) throw new IllegalStateException("close " + name);
        }
    }

    @Test
    void closesInReverseOrder() {
        List<String> log = new ArrayList<>();
        try (var a = new Res("a", log, false); var b = new Res("b", log, false)) {
            log.add("body");
        }
        assertEquals(List.of("body", "close b", "close a"), log);
    }

    @Test
    void bodyExceptionWinsAndCloseIsSuppressed() {
        List<String> log = new ArrayList<>();
        var ex = assertThrows(RuntimeException.class, () -> {
            try (var a = new Res("a", log, true)) {
                throw new RuntimeException("body");
            }
        });
        assertEquals("body", ex.getMessage());
        assertEquals("close a", ex.getSuppressed()[0].getMessage());
    }
}
java

Follow-up questions

What is the difference between AutoCloseable and Closeable? Closeable extends AutoCloseable, narrows the exception to IOException, and requires idempotent close().

Can you use try-with-resources with locks? Lock is not AutoCloseable. You can write a small wrapper whose close() calls unlock(), though the explicit lock() then try { } finally { unlock(); } pattern is more common and clearer.

Does try-with-resources replace finalize or Cleaner? Yes for deterministic cleanup. finalize is deprecated for removal; java.lang.ref.Cleaner is a safety net for resources that callers forget to close, not a substitute for closing them.

What happens if a resource is null? It is skipped; close() is not called and no NullPointerException occurs.

Interview exercise

Two resources are declared, A then B. The body throws E1, B.close() throws E2 and A.close() throws E3. What does the caller receive?

Answer and reasoning

The caller receives E1, with E2 and E3 attached as suppressed exceptions in that order. Resources close in reverse order, so B closes first and its E2 is added to E1 with addSuppressed; then A closes and its E3 is added as well. Both closes still run even though the first one failed, so neither resource leaks. If the body had completed normally, the caller would receive E2 (the first close failure) with E3 suppressed on it. This ordering is defined in the Java Language Specification section 14.20.3, and the reasoning shows you know that try-with-resources never lets a cleanup failure hide the real error.

Continue learning

Practise more with the Java interview questions and the Java MCQs. Related notes: exceptions and recovery boundaries, stream reuse and ownership and garbage collection and retained objects. The language rules are in the Java Language Specification, chapter 14, and the interface contract is in the AutoCloseable API documentation.

More in Java

read ✓Java · easy

Checked vs Unchecked Exceptions in Java

Checked exceptions must be caught or declared; unchecked ones needn't be. See the compiler rules, the lambda problem and when to use each.

~7 min readread →
read ✓Java · hard

Java CompletableFuture Error Composition

How exceptions travel through CompletableFuture chains, when to use exceptionally, handle or whenComplete, and why causes arrive wrapped.

~7 min readread →
esc