try-with-resources and AutoCloseable in Java

1. Overview

Files, sockets, database connections, and payment gateway sessions are all resources that must be closed after use. If you forget to close them, your application will leak file descriptors, exhaust the connection pool, or hold locks longer than necessary.

This article explains how to use try-with-resources to close resources automatically. You’ll also learn how to write your own class implementing AutoCloseable, how Java handles exceptions that occur during closing (suppressed exceptions), and common pitfalls when using this syntax.

2. Problems with try-finally

Before Java 7, the standard way to close resources was using finally blocks:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class LegacyOrderReader {

    public static String readFirstLine(String path) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(path));
        try {
            return reader.readLine();
        } finally {
            reader.close();
        }
    }
}

This code works, but it has two problems:

  • Verbose. When you have two or three resources, you must nest multiple try-finally blocks. Each block also needs null checks.
  • Masks the original exception. If readLine() throws an IOException, and then close() also throws an exception, the exception from finally replaces the original exception. You lose information about the real error.

try-with-resources solves both of these problems.

3. try-with-resources Syntax

You declare resources inside parentheses immediately after the try keyword:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class OrderReader {

    public static String readFirstLine(String path) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            return reader.readLine();
        }
    }
}

When the try block ends, Java automatically calls reader.close(). This happens whether the block completes normally, encounters a return, or throws an exception. You don’t need to write finally.

The only requirement is that the variable’s type must implement java.lang.AutoCloseable. Most I/O classes in the JDK satisfy this condition, such as InputStream, Reader, Connection, Statement, ResultSet, and Scanner.

3.1. Multiple Resources and Closing Order

You can declare multiple resources separated by semicolons. Java closes them in reverse order of declaration:

public class ClosingOrderDemo {

    record NamedResource(String name) implements AutoCloseable {
        NamedResource {
            System.out.println("Opening " + name);
        }

        @Override
        public void close() {
            System.out.println("Closing " + name);
        }
    }

    public static void main(String[] args) {
        try (var connection = new NamedResource("connection");
             var statement = new NamedResource("statement");
             var resultSet = new NamedResource("resultSet")) {
            System.out.println("Reading customer data");
        }
    }
}

Output:

Opening connection
Opening statement
Opening resultSet
Reading customer data
Closing resultSet
Closing statement
Closing connection

This order makes sense. Resources declared later typically depend on those declared earlier, like statement depends on connection, so they must be closed first.

One important point to remember: if the second resource throws an exception in its constructor, Java still closes the first resource that was successfully created. Resources that were never created won’t have close() called on them.

3.2. Using Existing Variables (Java 9+)

Starting with Java 9, you can include in try a variable already declared outside, as long as it’s final or effectively final:

import java.util.Scanner;

public class CustomerInput {

    public static void main(String[] args) {
        Scanner scanner = new Scanner("Nguyen Van A\n0901234567");
        try (scanner) {
            System.out.println("Name: " + scanner.nextLine());
            System.out.println("Phone: " + scanner.nextLine());
        }
    }
}

This is convenient when the resource comes from a parameter or is created by a complex factory.

3.3. catch and finally Run After Closing

try-with-resources still allows catch and finally. However, both run after the resources have been closed. Because of this, in catch you cannot use the resource again, and the resource variable is also out of scope.

4. Writing Your Own AutoCloseable Class

AutoCloseable has only one method:

void close() throws Exception;

Suppose you have a session with a payment gateway that needs to be released after each order:

public class PaymentGatewaySession implements AutoCloseable {

    private final String orderId;
    private boolean closed;

    public PaymentGatewaySession(String orderId) {
        this.orderId = orderId;
        System.out.println("Opening payment session for " + orderId);
    }

    public void charge(long amount) {
        if (closed) {
            throw new IllegalStateException("Session is closed");
        }
        if (amount <= 0) {
            throw new IllegalArgumentException("Invalid amount: " + amount);
        }
        System.out.println("Charging " + amount + " VND for order " + orderId);
    }

    public boolean isClosed() {
        return closed;
    }

    @Override
    public void close() {
        if (closed) {
            return;
        }
        closed = true;
        System.out.println("Closing payment session for " + orderId);
    }
}

There are three noteworthy points in this class:

  • close() does not declare throws Exception. An overriding method may narrow the list of checked exceptions. This way, users of your class are not forced to catch (Exception e).
  • close() is idempotent: calling it multiple times is safe and doesn’t release resources twice.
  • charge() checks the closed state to prevent using the resource after it’s closed.

Usage:

public class CheckoutService {

    public static void main(String[] args) {
        try (var session = new PaymentGatewaySession("ORD-1001")) {
            session.charge(250_000);
        }
    }
}
Opening payment session for ORD-1001
Charging 250000 VND for order ORD-1001
Closing payment session for ORD-1001

4.1. AutoCloseable or Closeable?

java.io.Closeable has existed since Java 5 and extends AutoCloseable. These two interfaces differ in:

AutoCloseable Closeable
close() Exception Exception IOException
Idempotent Recommended Mandatory by contract
Purpose Any type of resource I/O resources

If your resource involves I/O, use Closeable. For other cases, such as locks, sessions, or transactions, use AutoCloseable and narrow the throws clause as much as possible.

5. Suppressed Exceptions

Going back to the issue in section 2: the try block throws an exception, and then close() also throws an exception. With try-with-resources, the exception from the try block is kept as the primary exception. The exception from close() is attached to it as a suppressed exception:

public class SuppressedDemo {

    static class FlakyConnection implements AutoCloseable {
        @Override
        public void close() {
            throw new IllegalStateException("Error closing connection");
        }
    }

    public static void main(String[] args) {
        try (var connection = new FlakyConnection()) {
            throw new RuntimeException("Error saving order");
        } catch (RuntimeException e) {
            System.out.println("Primary: " + e.getMessage());
            for (Throwable suppressed : e.getSuppressed()) {
                System.out.println("Suppressed: " + suppressed.getMessage());
            }
        }
    }
}

Output:

Primary: Error saving order
Suppressed: Error closing connection

You keep both pieces of information. When you print the stack trace with e.printStackTrace() or through a logger, the Suppressed: section also appears. This makes debugging in production much easier.

If the try block runs normally and only close() throws an exception, then the exception from close() becomes the primary exception.

6. Testing Resource Closing Behavior

To write tests, add JUnit 5 and AssertJ:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.11.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.26.3</version>
    <scope>test</scope>
</dependency>

The tests below verify two main behaviors: resources are always closed even when errors occur, and exceptions from close() are attached to the primary exception as suppressed.

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class PaymentGatewaySessionTest {

    @Test
    void shouldCloseSessionEvenWhenChargeFails() {
        var session = new PaymentGatewaySession("ORD-2002");

        assertThatThrownBy(() -> {
            try (session) {
                session.charge(-5);
            }
        }).isInstanceOf(IllegalArgumentException.class)
          .hasMessageContaining("-5");

        assertThat(session.isClosed()).isTrue();
    }

    @Test
    void shouldAttachCloseFailureAsSuppressed() {
        AutoCloseable failingResource = () -> {
            throw new IllegalStateException("close failed");
        };

        assertThatThrownBy(() -> {
            try (failingResource) {
                throw new RuntimeException("business failed");
            }
        }).hasMessage("business failed")
          .satisfies(e -> assertThat(e.getSuppressed())
                  .singleElement()
                  .extracting(Throwable::getMessage)
                  .isEqualTo("close failed"));
    }

    @Test
    void closeShouldBeIdempotent() {
        var session = new PaymentGatewaySession("ORD-3003");
        session.close();
        session.close();

        assertThat(session.isClosed()).isTrue();
        assertThatThrownBy(() -> session.charge(100))
                .isInstanceOf(IllegalStateException.class);
    }
}

The second test uses a lambda to quickly create an AutoCloseable. This works because AutoCloseable has only one abstract method, so it can be used as a functional interface.

7. Common Pitfalls

7.1. Nesting Resources in Constructor

try (var in = new ObjectInputStream(new FileInputStream("orders.bin"))) {
    // ...
}

The constructor of ObjectInputStream reads the stream’s header and may throw IOException. At that point, the variable in hasn’t been assigned yet. The FileInputStream inside has been opened but will never be closed. The safe approach is to declare each resource separately:

try (var file = new FileInputStream("orders.bin");
     var in = new ObjectInputStream(file)) {
    // ...
}

7.2. Returning Resources Outside the try Block

public static Stream<String> loadOrderLines(Path path) throws IOException {
    try (Stream<String> lines = Files.lines(path)) {
        return lines; // Wrong: stream is closed when reaching the caller
    }
}

The caller will get an IllegalStateException as soon as they try to use the stream. You have two ways to fix this. First, process all data inside try, for example return lines.toList();. Second, return the stream outside and let the caller wrap it in try-with-resources themselves.

Note: Only Stream objects tied to I/O resources need to be closed, such as Files.lines, Files.list, or Files.walk. Streams created from List.stream() do not.

7.3. Throwing InterruptedException in close()

The Javadoc for AutoCloseable advises against throwing InterruptedException from close(). The reason is that this exception may be suppressed, losing the thread’s interrupt status. If you must wait in close(), catch the exception and call Thread.currentThread().interrupt().

7.4. Null Resources

Declaring try (var conn = findConnectionOrNull()) is legal. If the value is null, Java simply skips calling close(). However, this is usually a sign of poor design. You should use Optional or throw an exception from the factory.

7.5. When Not to Use It

Not every object implementing AutoCloseable should be closed immediately within a method. For example, in Spring Boot, shared DataSource or HttpClient are beans that live as long as the application context. Spring closes them when the context shuts down. If you wrap a shared bean in try-with-resources, you’ll close it while other components are still using it.

Only use try-with-resources for resources that your code creates and owns within that scope.

7.6. Tip: ExecutorService in Java 21

Starting with Java 19, ExecutorService implements AutoCloseable. The close() method waits for all tasks to complete. This is very convenient when using virtual threads:

import java.util.concurrent.Executors;

public class InvoiceBatch {

    public static void main(String[] args) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 1; i <= 3; i++) {
                int invoiceId = i;
                executor.submit(() -> System.out.println("Sending invoice #" + invoiceId));
            }
        }
        System.out.println("All invoices sent");
    }
}

The line “All invoices sent” is always printed last, because close() blocks until all three tasks complete. The order of the three invoices, however, is not guaranteed.

8. Conclusion

try-with-resources makes code shorter and safer than try-finally. Resources are always closed in reverse order of opening. The original exception is not hidden, thanks to the suppressed exception mechanism.

When writing your own AutoCloseable class, you should:

  • narrow the throws clause of close();
  • make close() idempotent;
  • prevent the object from being used after closing.

Also, avoid nesting resources in constructors, don’t return resources outside the try block, and only close resources that your code actually owns.

javatry-with-resourcesautocloseableexceptionbest-practices