☕ Java
try-with-resources
Try-with-resources is a Java 7 feature that automatically closes resources when a try block exits, whether normally or due to an exception. Any object implementing AutoCloseable can be declared in the try statement's resource list and will have its close() method called automatically. This eliminates the verbose and error-prone try-finally pattern for resource management, correctly handles the case where both the body and close() throw exceptions through exception suppression, and closes multiple resources in the correct reverse order. This entry covers the full mechanics, exception suppression semantics, multiple resource management, the AutoCloseable contract, implementing custom closeable resources, and the pitfalls to avoid.
Mechanics — What the JVM Does
Try-with-resources is syntactic sugar — the compiler transforms it into a try-finally block with careful exception handling. Understanding the transformation reveals exactly what guarantees the feature provides and what pitfalls remain.
The resource declaration in the try statement creates effectively-final variables. Each resource is initialised in order, and the JVM guarantees that close() will be called on every successfully initialised resource, even if initialising a later resource throws. If resource A initialises successfully but resource B throws during initialisation, A will still be closed. The closing order is always the reverse of the opening order, which is correct for resources that depend on each other — an OutputStream wrapping a FileOutputStream should be closed before the FileOutputStream it wraps.
The resource variables declared in the try header are in scope within the try block but not in catch or finally blocks attached to the same try statement. This is a deliberate restriction: by the time catch or finally blocks run, the resources have already been closed. If you need to access the resource in a catch block, you must close it manually or restructure the code.
Exception suppression is the mechanism that makes try-with-resources strictly better than manual finally for resource management. If the try body throws exception E1 and close() throws exception E2, the manual finally approach loses E1 (finally's exception replaces the body's exception). Try-with-resources preserves both: E1 propagates normally, and E2 is recorded as a suppressed exception accessible via E1.getSuppressed().
Java
// ── What the compiler generates from try-with-resources ─────────────
// Source code you write:
try (InputStream in = new FileInputStream("input.txt")) {
process(in);
}
// Compiler generates approximately:
InputStream in = new FileInputStream("input.txt");
Throwable primaryException = null;
try {
process(in);
} catch (Throwable t) {
primaryException = t;
throw t;
} finally {
if (in != null) {
if (primaryException != null) {
try {
in.close();
} catch (Throwable closeEx) {
// close() exception SUPPRESSED — not lost!
primaryException.addSuppressed(closeEx);
}
} else {
in.close(); // no body exception — close() can propagate normally
}
}
}
// ── Resource variable scope — NOT accessible in catch/finally ─────────
try (Connection conn = dataSource.getConnection()) {
processWithConnection(conn);
} catch (SQLException e) {
// conn is NOT accessible here — already closed
// log.info(conn.getSchema()); // COMPILE ERROR — conn out of scope
log.error("DB error", e);
}
// ── Resources in reverse close order ──────────────────────────────────
try (
FileOutputStream fos = new FileOutputStream("out.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos);
PrintWriter pw = new PrintWriter(bos)
) {
pw.println("Hello World");
}
// Close order: pw.close() → bos.close() → fos.close()
// PrintWriter must flush before BufferedOutputStream,
// which must flush before FileOutputStream — reverse order is correctException Suppression — Handling Dual Failures
Exception suppression is the crucial difference between try-with-resources and manual try-finally for resource management. In the manual pattern, if the try body throws exception B and the finally block's close() call also throws exception C, only C propagates — B is silently discarded, replaced by C. The original failure is lost, making debugging very difficult: you see the cleanup failure (C) rather than the actual problem (B) that caused the failure.
Try-with-resources handles this correctly: the body's exception B is the primary exception and always propagates. If close() also throws C, C is added to B's suppressed exception list by calling B.addSuppressed(C). Both exceptions are preserved. The caller catches B, and can inspect the suppressed exceptions via B.getSuppressed() to see that closing also failed. Neither exception is lost.
Suppressed exceptions appear in the stack trace output when printed, prefixed with "Suppressed:" below the primary exception's frames. This is standard Java exception printing behaviour — no extra code is needed to see suppressed exceptions in logs.
Understanding suppression also reveals when not to use try-with-resources: if you actually want to catch and handle close() failures separately from body failures, manual try-catch-finally may be clearer, though this is rare in practice.
Java
// ── Suppression demonstration ─────────────────────────────────────────
public class FailingResource implements AutoCloseable {
private final String name;
public FailingResource(String name) {
System.out.println("Opening " + name);
this.name = name;
}
public void doWork() {
throw new RuntimeException(name + ": body failed");
}
@Override
public void close() {
throw new RuntimeException(name + ": close() failed");
}
}
// ── Without try-with-resources — close() exception REPLACES body ──────
FailingResource r = null;
try {
r = new FailingResource("manual");
r.doWork(); // throws "body failed"
} finally {
if (r != null) r.close(); // throws "close() failed" — REPLACES body exception!
}
// Caller sees: "close() failed" — "body failed" is LOST
// ── With try-with-resources — both preserved ──────────────────────────
try (FailingResource r2 = new FailingResource("auto")) {
r2.doWork(); // throws "body failed" — primary exception
}
// Caller catches: "body failed" (primary)
// Suppressed: "close() failed" (visible via getSuppressed())
// ── Accessing suppressed exceptions ───────────────────────────────────
try {
try (FailingResource r3 = new FailingResource("demo")) {
r3.doWork();
}
} catch (RuntimeException e) {
System.out.println("Primary: " + e.getMessage());
// "demo: body failed"
Throwable[] suppressed = e.getSuppressed();
System.out.println("Suppressed count: " + suppressed.length); // 1
System.out.println("Suppressed: " + suppressed[0].getMessage());
// "demo: close() failed"
}
// ── Stack trace output with suppressed exception ──────────────────────
// java.lang.RuntimeException: demo: body failed
// at FailingResource.doWork(...)
// at Main.main(...)
// Suppressed: java.lang.RuntimeException: demo: close() failed
// at FailingResource.close(...)
// at Main.main(...)
// Both exceptions visible in the output — nothing lostMultiple Resources and Initialisation Failures
Multiple resources are declared in a single try statement separated by semicolons. They are initialised left-to-right in the order they appear, and closed right-to-left (reverse order) when the block exits. This ordering guarantees that dependent resources are closed before the resources they depend on — a BufferedOutputStream wrapping a FileOutputStream is always closed (and flushed) before the underlying FileOutputStream.
When one resource fails to initialise, all previously initialised resources are immediately closed. If you open resource A, resource B, and then resource C's constructor throws, B and A are closed (in that order) before the exception propagates. This guarantee prevents resource leaks even when initialisation is partially successful.
Java 9 introduced the ability to use effectively final variables in the resource list — variables that were declared before the try statement and not reassigned afterward. This allows resources that are created through factory methods or passed in as parameters to be used in try-with-resources without the awkward wrapping pattern of declaring them inside the try header when they were already available.
Java
// ── Multiple resources — declared left to right, closed right to left ─
try (
Connection conn = dataSource.getConnection(); // opened 1st
PreparedStatement stmt = conn.prepareStatement(SQL); // opened 2nd
ResultSet rs = stmt.executeQuery() // opened 3rd
) {
while (rs.next()) {
process(rs);
}
}
// Closed: rs.close() — 3rd opened, 1st closed
// stmt.close() — 2nd opened, 2nd closed
// conn.close() — 1st opened, 3rd closed
// Correct: ResultSet closed before Statement, Statement before Connection
// ── Partial initialisation — already-opened resources are closed ───────
try (
InputStream in = new FileInputStream("input.txt"); // succeeds
OutputStream out = new FileOutputStream("/no/perms") // THROWS
) {
// body — never reached
}
// Even though body never ran:
// out never opened (constructor threw)
// in WAS opened — JVM closes in BEFORE propagating the constructor exception
// No resource leak
// ── Java 9+: effectively final variables in resource list ────────────
Connection conn = acquireConnection(); // created before the try
try (conn) { // use existing variable
// conn used here
} // conn.close() called automatically
// Equivalent to:
try (Connection autoClose = conn) { // pre-Java-9 workaround
// autoClose used here
}
// ── Combining try-with-resources with catch and finally ───────────────
// catch and finally are ALLOWED after the resource list:
try (InputStream in = new FileInputStream("file.txt")) {
processStream(in);
} catch (FileNotFoundException e) {
log.warn("File not found, skipping: {}", e.getMessage());
} catch (IOException e) {
log.error("IO error processing file", e);
} finally {
log.debug("File processing attempt complete");
}
// Resource (in) is closed BEFORE catch and finally blocks run
// ── Nested try-with-resources ─────────────────────────────────────────
// Sometimes inner resources must be opened conditionally:
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
try {
processTransaction(conn);
conn.commit();
} catch (Exception e) {
conn.rollback();
throw e;
}
}Implementing AutoCloseable — Custom Resources
Any class can participate in try-with-resources by implementing AutoCloseable, which has a single method: void close() throws Exception. The more specific Closeable (used by all I/O classes) extends AutoCloseable and declares close() throws IOException. For domain objects that manage resources, implementing AutoCloseable makes the cleanup contract explicit and automatic.
The close() method contract has several important aspects. It should be idempotent: calling close() multiple times should have the same effect as calling it once. A second close() should do nothing rather than throwing. This requirement exists because framework code may call close() defensively multiple times, and the JVM's try-with-resources implementation may call it in both the normal path and the suppression path. The idempotency guarantee prevents double-close errors.
The close() method should not throw checked exceptions unless there is genuinely a recoverable failure mode in closing. Most resources either close successfully or fail in ways that cannot be recovered. If close() failure is non-recoverable, throwing a RuntimeException (unchecked) is more appropriate than forcing callers to handle a checked exception from the close() call. The AutoCloseable's close() method declares throws Exception, but implementing classes can narrow this to a more specific type or remove the throws declaration entirely.
Java
// ── Custom AutoCloseable — database transaction ───────────────────────
public class ManagedTransaction implements AutoCloseable {
private final Connection connection;
private boolean committed = false;
private boolean closed = false;
public ManagedTransaction(DataSource ds) throws SQLException {
this.connection = ds.getConnection();
this.connection.setAutoCommit(false);
}
public Connection getConnection() {
assertOpen();
return connection;
}
public void commit() throws SQLException {
assertOpen();
connection.commit();
committed = true;
}
@Override
public void close() throws SQLException {
if (closed) return; // idempotent — safe to call multiple times
closed = true;
try {
if (!committed) {
connection.rollback(); // auto-rollback if not committed
}
} finally {
connection.setAutoCommit(true);
connection.close();
}
}
private void assertOpen() {
if (closed) throw new IllegalStateException(
"Transaction is already closed");
}
}
// Usage:
try (ManagedTransaction tx = new ManagedTransaction(dataSource)) {
orderRepo.save(order, tx.getConnection());
inventoryRepo.update(items, tx.getConnection());
tx.commit();
} // auto-rollback if commit() was not called (exception occurred)
// ── Custom AutoCloseable — lock management ────────────────────────────
public class AutoLock implements AutoCloseable {
private final Lock lock;
public static AutoLock acquire(Lock lock) {
lock.lock();
return new AutoLock(lock);
}
private AutoLock(Lock lock) {
this.lock = lock;
}
@Override
public void close() {
lock.unlock(); // no checked exception — clean close contract
}
}
// Usage — guaranteed unlock even if body throws:
ReentrantLock myLock = new ReentrantLock();
try (AutoLock ignored = AutoLock.acquire(myLock)) {
criticalSection();
} // myLock.unlock() called automatically
// ── Closeable contract — idempotency requirement ──────────────────────
public class SafeCloseable implements AutoCloseable {
private final InputStream underlying;
private boolean closed = false;
public SafeCloseable(InputStream s) { this.underlying = s; }
@Override
public void close() throws IOException {
if (!closed) { // idempotent guard
closed = true;
underlying.close();
}
// Second call does nothing — no exception thrown
}
}Related Topics in Exception Handling
Exception Basics
An exception is an event that disrupts the normal flow of a program during execution. Java's exception system is a structured mechanism for signalling, propagating, and handling error conditions — a significant improvement over the older approach of returning special error codes that callers could silently ignore. Understanding exceptions means understanding the class hierarchy that categorises them, the distinction between checked and unchecked exceptions, what the JVM does when an exception is thrown, how the call stack is unwound, and what information an exception object carries. This entry covers the full exception hierarchy, the checked vs unchecked distinction and the reasoning behind it, exception propagation through the call stack, and the information model of exception objects.
try-catch
The try-catch statement is Java's primary mechanism for handling exceptions. Code that might throw an exception is placed in the try block; one or more catch blocks follow, each specifying the exception type to handle and providing the handling logic. When an exception is thrown inside the try block, execution of the try block immediately stops and the JVM searches the catch blocks in order for the first one whose type matches the thrown exception. Understanding the control flow precisely — what runs, what stops, and in what order — is essential for writing correct exception handling code. This entry covers try-catch control flow, exception type matching, catching by supertype, variable scope, and the key principles for writing good catch blocks.
Multiple catch
A single try block can be followed by multiple catch blocks, each handling a different exception type. Multiple catch blocks allow code to respond differently to different kinds of failures — recovering from an expected missing file, but reporting and rethrowing an unexpected database error. Java 7 introduced multi-catch syntax, allowing a single catch block to handle multiple unrelated exception types, eliminating the duplicate code that arises when different exceptions require the same handling. This entry covers the ordering rules, multi-catch syntax, the type of the caught variable in multi-catch, and the design patterns for structuring multiple exception handlers.
finally
The finally block contains code that must execute regardless of whether the try block completed normally, threw an exception that was caught, or threw an exception that was not caught. It is the mechanism for guaranteed cleanup — closing files, releasing locks, returning connections to a pool, rolling back transactions. The finally block runs in all scenarios except two: if the JVM exits (System.exit()) or if the thread is killed by an Error like StackOverflowError. Understanding when exactly finally runs, the interaction between finally and return statements, exception suppression when finally itself throws, and the modern alternative of try-with-resources is essential for writing correct resource management code in Java.