try-with-resources and cleanup

AutoCloseable, suppressed exceptions, finally's foot-guns, and the resource leak that shows up as a connection pool exhausted at 3am.

3 min read🧰 Exceptions, I/O and Reflection

A resource is anything you must give back: a file handle, a socket, a database connection, a lock. Forgetting to give it back is a leak, and a leak in a server is not a crash — it is a service that works for three days and then stops accepting connections. try-with-resources is the mechanism that makes giving back automatic, and it has two subtleties that the finally block it replaced could never get right.

The old way and why it was wrong

java
InputStream in = null;
try {
    in = Files.newInputStream(path);
    process(in);
} finally {
    if (in != null) in.close();       // if close() throws, the original exception is lost
}

Three problems. It is verbose. If process throws and close throws, the exception from close replaces the one from process, and you debug the wrong failure. And with two resources, the nesting doubles.

try-with-resources

java
try (InputStream in = Files.newInputStream(path);
     BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) {
    return reader.lines().count();
}

Anything that implements AutoCloseable can be declared in the parentheses. The compiler generates the close calls — in reverse order of declaration, so reader closes before in — whether the block completes, returns, or throws. Since Java 9 an effectively-final variable declared earlier can be used: try (in) { ... }.

Suppressed exceptions

If the body throws and a close() also throws, the body's exception is the one propagated, and the close() exception is attached to it as suppressed:

java
try (var r = new Resource()) {
    throw new IllegalStateException("body failed");
}
// caught: IllegalStateException: body failed
//   Suppressed: java.io.IOException: close failed

e.getSuppressed() returns them; stack traces print them. The primary failure is preserved, and the secondary one is not lost. This is the behaviour the manual finally could not produce.

finally's remaining uses and its foot-guns

finally still exists for cleanup that is not a resource — resetting a flag, restoring a thread's context class loader, releasing a lock acquired with lock() (which is not AutoCloseable). Two things to never do in it:

  • return in finally. It discards any exception thrown in the body, silently. Same for break and continue.
  • Throwing from finally. It replaces the in-flight exception, the same loss try-with-resources was designed to prevent.
java
lock.lock();
try {
    mutate();
} finally {
    lock.unlock();      // correct: the one thing finally is still for
}

Implementing AutoCloseable

Your own classes that own resources should implement it, so callers can use them in try:

java
public final class BatchWriter implements AutoCloseable {
    private final Connection conn;
    private boolean closed;
 
    @Override public void close() {
        if (closed) return;             // idempotent: closing twice is harmless
        closed = true;
        conn.close();
    }
}

close() should be idempotent, should release everything even if one release fails (catch, add as suppressed, continue), and should not throw a checked exception unless it genuinely must — AutoCloseable.close() declares throws Exception, but you can narrow it to nothing, which spares every caller a catch.

The leak that shows up at 3am

java
public List<Order> load(long customerId) {
    Connection c = dataSource.getConnection();               // borrowed from the pool
    PreparedStatement ps = c.prepareStatement(SQL);
    ps.setLong(1, customerId);
    ResultSet rs = ps.executeQuery();                          // throws on a bad row
    ...
    c.close();                                                // never reached on the throw
}

One bad row means one connection never returned. HikariCP's default pool is ten. Ten bad rows over a day and every request waits thirty seconds for a connection that will never come, then fails with SQLTransientConnectionException: connection is not available. The service is up; every endpoint that touches the database is down. This is the single most common resource leak in Java services, and the fix is the three-resource try you would write on the first day of a Java course.

Streams that are resources

Files.lines(path), Files.list(dir), Files.walk return streams that hold an open handle. Close them: try (Stream<String> lines = Files.lines(path)) { ... }. A stream from a collection does not need this; a stream from the file system does.

Progress is saved on this device and to your account when signed in.