Contents
- Before & After try-with-resources
- AutoCloseable vs Closeable
- Multiple Resources
- Suppressed Exceptions
- Custom AutoCloseable Types
- Effectively Final Resources (Java 9+)
- Common Pitfalls
The problem try-with-resources solves is apparent when you look at the pre-Java 7 code required to safely read a file. Every resource needs its own finally block, and each close() call can itself throw, requiring more nesting. With two or three resources the code becomes unreadable.
// PRE-JAVA 7 — verbose, error-prone resource management
public String readFileLegacy(String path) throws Exception {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(path));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n');
}
return sb.toString();
} finally {
if (reader != null) {
try {
reader.close(); // close() itself can throw — must handle separately
} catch (IOException e) {
// suppress, log, or rethrow? — no good answer here
}
}
}
}
// JAVA 7+ — try-with-resources: clean, correct, and concise
public String readFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n');
}
return sb.toString();
}
// reader.close() is called automatically, always, even if an exception occurs
}
The resource variable declared in the try header is implicitly final (or effectively final as of Java 9) and scoped to the try block. You cannot reassign it inside the block.
Any class that implements java.lang.AutoCloseable can be used in a try-with-resources statement. java.io.Closeable is a more specific subinterface (for I/O resources) that narrows close() to throw only IOException and requires close() to be idempotent (safe to call multiple times). Use the right one for your type.
| AutoCloseable | Closeable |
| Package | java.lang | java.io |
| Exception thrown by close() | Any Exception | IOException only |
| Idempotent close required? | No (recommended) | Yes (required) |
| Use for | Any resource (DB, locks, transactions) | I/O streams, channels, readers |
import java.io.*;
// Closeable — for I/O resources, close() throws IOException
public class CsvWriter implements Closeable {
private final BufferedWriter writer;
public CsvWriter(String path) throws IOException {
this.writer = new BufferedWriter(new FileWriter(path));
}
public void writeRow(String... columns) throws IOException {
writer.write(String.join(",", columns));
writer.newLine();
}
@Override
public void close() throws IOException {
writer.close(); // flush + close
}
}
// AutoCloseable — for non-I/O resources, close() can throw any Exception
public class DatabaseTransaction implements AutoCloseable {
private final java.sql.Connection connection;
private boolean committed = false;
public DatabaseTransaction(java.sql.Connection conn) throws java.sql.SQLException {
this.connection = conn;
this.connection.setAutoCommit(false);
}
public void commit() throws java.sql.SQLException {
connection.commit();
committed = true;
}
@Override
public void close() throws java.sql.SQLException {
if (!committed) {
connection.rollback(); // automatic rollback if not committed
}
connection.setAutoCommit(true);
}
}
// Usage of both
try (CsvWriter csv = new CsvWriter("output.csv")) {
csv.writeRow("id", "name", "email");
csv.writeRow("1", "Alice", "alice@example.com");
} // csv.close() called automatically
// try (DatabaseTransaction tx = new DatabaseTransaction(conn)) {
// tx.commit();
// } // auto-rollback if commit() wasn't called
You can declare multiple resources in a single try header, separated by semicolons. They are opened in left-to-right order and closed in reverse order — last opened, first closed. This mirrors the stack discipline you'd write manually and ensures outer wrappers are closed before the inner resources they depend on.
import java.io.*;
import java.nio.file.*;
// Multiple resources — opened L-to-R, closed R-to-L
public void copyFile(Path source, Path target) throws IOException {
try (
InputStream in = Files.newInputStream(source);
OutputStream out = Files.newOutputStream(target)
) {
in.transferTo(out);
}
// Closing order: out.close() first, then in.close()
}
// Three-resource example with decorators
public void processGzipFile(String gzipPath, String outputPath) throws IOException {
try (
FileInputStream fis = new FileInputStream(gzipPath);
java.util.zip.GZIPInputStream gzip = new java.util.zip.GZIPInputStream(fis);
BufferedReader reader = new BufferedReader(new InputStreamReader(gzip));
BufferedWriter writer = new BufferedWriter(new FileWriter(outputPath))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line.toUpperCase());
writer.newLine();
}
}
// Close order: writer → reader → gzip → fis
// Each close() flushes and releases, ensuring no data is lost
}
Separating resource declarations with semicolons is more readable than deeply nesting them. It also ensures that even if one close() throws, the remaining resources are still closed — something deeply nested constructors cannot guarantee.
What happens if an exception is thrown from the try block and another exception is thrown from close()? In pre-Java 7 code this was a nightmare — the close exception would silently swallow the original exception. Try-with-resources solves this: the primary exception propagates normally, and any exceptions from close() are attached to it as suppressed exceptions, retrievable via Throwable.getSuppressed().
public class BrokenResource implements AutoCloseable {
private final String name;
public BrokenResource(String name) { this.name = name; }
public void use() throws Exception {
throw new Exception(name + ": error during use");
}
@Override
public void close() throws Exception {
throw new Exception(name + ": error during close");
}
}
public class SuppressedDemo {
public static void main(String[] args) {
try (BrokenResource r = new BrokenResource("R1")) {
r.use();
} catch (Exception e) {
System.out.println("Primary: " + e.getMessage());
// R1: error during use
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Suppressed: " + suppressed.getMessage());
// Suppressed: R1: error during close
}
}
}
}
// With multiple resources, each close() exception is suppressed onto the primary
try (
BrokenResource r1 = new BrokenResource("R1");
BrokenResource r2 = new BrokenResource("R2")
) {
r1.use(); // throws primary exception
}
// R2.close() throws → suppressed[0]
// R1.close() throws → suppressed[1]
// primary: "R1: error during use"
Always check getSuppressed() when debugging resource-related failures. Frameworks and loggers that only print e.getMessage() will hide the suppressed exceptions. When logging exceptions, use a method that calls printStackTrace() or a logging framework that serialises the full exception chain including suppressed entries.
Any resource that needs deterministic cleanup — not just I/O streams — benefits from implementing AutoCloseable. Common candidates include database connections and transactions, thread pool executors, distributed locks, network connections, and temporary files.
import java.util.concurrent.*;
// Wrapping an ExecutorService as AutoCloseable (Java 18 added this natively)
public class ManagedExecutor implements AutoCloseable {
private final ExecutorService executor;
public ManagedExecutor(int threads) {
this.executor = Executors.newFixedThreadPool(threads);
}
public <T> Future<T> submit(Callable<T> task) {
return executor.submit(task);
}
@Override
public void close() {
executor.shutdown();
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
// A temporary file that cleans itself up
public class TempFile implements AutoCloseable {
private final java.nio.file.Path path;
public TempFile(String prefix, String suffix) throws java.io.IOException {
this.path = java.nio.file.Files.createTempFile(prefix, suffix);
}
public java.nio.file.Path getPath() { return path; }
@Override
public void close() throws java.io.IOException {
java.nio.file.Files.deleteIfExists(path);
}
}
// Usage
try (
ManagedExecutor exec = new ManagedExecutor(4);
TempFile tmp = new TempFile("work-", ".json")
) {
Future<String> result = exec.submit(() -> {
java.nio.file.Files.writeString(tmp.getPath(), "{\"status\":\"ok\"}");
return java.nio.file.Files.readString(tmp.getPath());
});
System.out.println(result.get()); // {"status":"ok"}
}
// tmp deleted, executor shut down — automatically
Java 9 relaxed the requirement that resources must be declared inside the try header. You can now use an effectively final variable that was declared before the try statement. This avoids redundant variable declarations when you already have a reference you want to close.
// Java 7/8 — had to redeclare the variable in the try header
InputStream stream = openStream();
try (InputStream s = stream) { // redundant — two names for same object
process(s);
}
// Java 9+ — use the existing effectively-final variable directly
InputStream stream = openStream();
try (stream) { // clean — no re-declaration needed
process(stream);
}
// Practical example — factory method returns an AutoCloseable
public Connection createConnection(String url) throws Exception { /* ... */ return null; }
Connection conn = createConnection("jdbc:postgresql://localhost/mydb");
try (conn) {
conn.prepareStatement("SELECT 1").execute();
}
// conn.close() called automatically
// Note: the variable MUST be effectively final
// This is illegal:
InputStream s = openStream();
s = openOtherStream(); // reassignment — no longer effectively final
// try (s) { ... } // COMPILE ERROR
Try-with-resources is straightforward but has a few traps worth knowing:
// PITFALL 1: Wrapping in the try header loses the inner resource on exception
// If GZIPInputStream constructor throws, fis is never assigned to any twr variable
// and is silently leaked in this pattern:
try (BufferedReader r = new BufferedReader(
new InputStreamReader(
new java.util.zip.GZIPInputStream(
new FileInputStream("data.gz"))))) { // if GZIPInputStream() throws,
// ... // FileInputStream leaks!
}
// SAFE: declare each resource separately so each is registered for close()
try (
FileInputStream fis = new FileInputStream("data.gz");
java.util.zip.GZIPInputStream gzip = new java.util.zip.GZIPInputStream(fis);
InputStreamReader isr = new InputStreamReader(gzip);
BufferedReader br = new BufferedReader(isr)
) {
br.lines().forEach(System.out::println);
}
// PITFALL 2: null resources cause NullPointerException in close()
// try-with-resources calls close() even if the resource is null — NPE!
// Fix: ensure resources are non-null before the try, or guard in close()
// (Closeable/AutoCloseable does NOT require null-safety)
// PITFALL 3: Returning from inside try-with-resources — is close() still called?
// YES — close() is always called. The return value is unaffected.
public int safeReturn() throws Exception {
try (AutoCloseable r = openResource()) {
return 42; // close() runs AFTER 42 is captured but BEFORE method returns
}
}
// PITFALL 4: close() swallowing exceptions — if close() catches and ignores
// its own exceptions, suppressed exception info is permanently lost.
// Always propagate or at least log inside close().
Never nest resource constructors inside the try-with-resources header. Always declare each resource on its own line so every one is registered for automatic closing. This single rule prevents the most common resource-leak bug in Java I/O code.