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-finallyblocks. Each block also needs null checks. - Masks the original exception. If
readLine()throws anIOException, and thenclose()also throws an exception, the exception fromfinallyreplaces 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 declarethrows Exception. An overriding method may narrow the list of checked exceptions. This way, users of your class are not forced tocatch (Exception e).close()is idempotent: calling it multiple times is safe and doesn’t release resources twice.charge()checks theclosedstate 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
Streamobjects tied to I/O resources need to be closed, such asFiles.lines,Files.list, orFiles.walk. Streams created fromList.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
throwsclause ofclose(); - 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.