Java try-with-resources

Su Suriya Ravichandran Updated 14 Nov 2026
12 min read ·Lesson 132 of 133

The Problem It Solves

Many objects hold an operating-system or external resource: an open file, a network socket, a database connection, a lock on a log file. These are limited. An operating system allows a process only so many open files; a connection pool has a fixed number of database connections. If a program opens resources and doesn't close them, it eventually fails with errors such as Too many open files or waits forever for a free connection. This is called a resource leak.

Closing a resource correctly by hand is harder than it looks. It must be closed on every path — normal completion, return and every exception — and closing can itself fail. Before Java 7, correct code looked like this:

BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("orders.csv"));
    return reader.readLine();
} finally {
    if (reader != null) {
        reader.close();   // if this throws, it hides any exception from the try block
    }
}

try-with-resources (Java 7+) makes the compiler generate all of this correctly for you.

Syntax

try (ResourceType name = new ResourceType(...)) {
    // use the resource
}
// the resource is closed here, whatever happened above

The parentheses after try contain one or more resource declarations. When the try block finishes — normally, by return, break, continue or an exception — Java automatically calls close() on each resource.

try (BufferedReader reader = Files.newBufferedReader(Path.of("orders.csv"))) {
    return reader.readLine();
}

This is equivalent to the longer manual version above, but shorter and more correct, as the following sections show.

The AutoCloseable Interface

A resource in a try-with-resources statement must implement java.lang.AutoCloseable:

public interface AutoCloseable {
    void close() throws Exception;
}

java.io.Closeable (used by streams, readers and writers) is a sub-interface whose close() throws IOException. Almost every JDK class that holds a resource implements one of them: InputStream, OutputStream, Reader, Writer, Scanner, Socket, ServerSocket, java.sql.Connection, Statement, ResultSet, java.util.stream.Stream (relevant for Files.lines), ExecutorService (Java 19+), and many more.

Using a type that doesn't implement it is a compile error:

public class NotCloseable {
    public static void main(String[] args) {
        try (StringBuilder sb = new StringBuilder()) {
            sb.append("x");
        }
    }
}

Compiler output:

NotCloseable.java:3: error: incompatible types: try-with-resources not applicable to variable type
        try (StringBuilder sb = new StringBuilder()) {
                           ^
    (StringBuilder cannot be converted to AutoCloseable)
1 error

A StringBuilder holds only memory, which the garbage collector reclaims, so it has nothing to close.

Closing Order, Exceptions, catch and finally

The following program uses a small class that prints when it is opened, used and closed, so you can see exactly what happens and when.

public class Order {
    static class Resource implements AutoCloseable {
        private final String name;

        Resource(String name) {
            this.name = name;
            System.out.println("open  " + name);
        }

        void use(boolean fail) {
            System.out.println("use   " + name);
            if (fail) throw new IllegalStateException(name + " failed");
        }

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

    public static void main(String[] args) {
        System.out.println("== success ==");
        try (Resource db = new Resource("db"); Resource file = new Resource("file")) {
            db.use(false);
            file.use(false);
        }

        System.out.println("== failure ==");
        try (Resource db = new Resource("db"); Resource file = new Resource("file")) {
            db.use(false);
            file.use(true);
            System.out.println("not reached");
        } catch (IllegalStateException e) {
            System.out.println("catch " + e.getMessage());
        } finally {
            System.out.println("finally");
        }
    }
}

Output:

== success ==
open  db
open  file
use   db
use   file
close file
close db
== failure ==
open  db
open  file
use   db
use   file
close file
close db
catch file failed
finally

What this shows:

  1. Multiple resources are separated by semicolons and are opened from left to right.
  2. They are closed in reverse order — file before db. This matters because later resources often depend on earlier ones: a ResultSet depends on its Statement, which depends on its Connection, so they must be closed innermost first.
  3. Resources are closed before catch runs. In the failure case, both resources are already closed when the catch block prints its message. The handler can't use them, and by the time error handling happens, nothing is still held open.
  4. finally runs last, after the resources are closed and after catch.
  5. close() is declared without throws. An implementation may narrow the throws Exception of AutoCloseable.close(). Because this close() throws no checked exception, the first try needs no catch. With a resource whose close() throws IOException, the code would have to catch or declare IOException.

If a resource's constructor fails, the resources declared before it are closed, and the ones after it are never created. Only successfully initialised resources are closed.

Suppressed Exceptions

The most important advantage over a manual finally is how it handles a failure during close() when the body has already failed.

public class Suppressed {
    static class Connection implements AutoCloseable {
        void send(String data) {
            throw new IllegalStateException("send failed: network down");
        }

        @Override
        public void close() {
            throw new IllegalStateException("close failed: socket already reset");
        }
    }

    public static void main(String[] args) {
        try (Connection c = new Connection()) {
            c.send("order #42");
        } catch (IllegalStateException e) {
            System.out.println("primary:    " + e.getMessage());
            for (Throwable s : e.getSuppressed()) {
                System.out.println("suppressed: " + s.getMessage());
            }
        }
    }
}

Output:

primary:    send failed: network down
suppressed: close failed: socket already reset

Two exceptions happen: send fails, and then the automatic close() fails too. Java keeps the first one — the real cause of the problem — as the exception that propagates, and attaches the close failure to it using Throwable.addSuppressed. You retrieve those with getSuppressed(), which returns an array (empty if nothing was suppressed).

Compare this with the manual finally version from the Finally Block lesson, where the cleanup exception replaced the original and the root cause was lost.

If only close() fails (the body succeeds), the close exception propagates normally as the primary exception.

Suppressed exceptions also appear in stack traces. When the exception isn't caught:

public class Trace {
    static class Connection implements AutoCloseable {
        void send() { throw new IllegalStateException("send failed"); }
        @Override public void close() { throw new IllegalStateException("close failed"); }
    }

    public static void main(String[] args) {
        try (Connection c = new Connection()) {
            c.send();
        }
    }
}

Output:

Exception in thread "main" java.lang.IllegalStateException: send failed
	at Trace$Connection.send(Trace.java:3)
	at Trace.main(Trace.java:9)
	Suppressed: java.lang.IllegalStateException: close failed
		at Trace$Connection.close(Trace.java:4)
		at Trace.main(Trace.java:8)

The indented Suppressed: section belongs to the primary exception above it. When reading production logs, always check for it: it often explains why a cleanup step, such as committing or flushing data, also failed.

Scope of Resource Variables

A resource variable declared in the try header is visible only inside the try block:

import java.io.*;

public class Scope {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new StringReader("a"))) {
            System.out.println(reader.readLine());
        } catch (IOException e) {
            System.out.println(reader);
        }
    }
}

Compiler output:

Scope.java:8: error: cannot find symbol
            System.out.println(reader);
                               ^
  symbol:   variable reader
  location: class Scope
1 error

This is intentional: by the time catch or finally runs, the resource is already closed, so using it there would be a bug. Resource variables are also implicitly final — you can't assign a different object to them inside the block, which guarantees that the object closed is the object that was opened.

Using an Existing Variable (Java 9+)

Since Java 9, the header can refer to a resource variable declared earlier, as long as it is final or effectively final (never reassigned):

import java.io.*;

public class Java9 {
    static String firstLine(BufferedReader reader) throws IOException {
        try (reader) {
            return reader.readLine();
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedReader r = new BufferedReader(new StringReader("header,row\n1,2"));
        System.out.println(firstLine(r));
        try {
            r.readLine();
        } catch (IOException e) {
            System.out.println("after try: " + e.getMessage());
        }
    }
}

Output:

header,row
after try: Stream closed

try (reader) takes ownership of the reader that was passed in and closes it at the end. Reading from it afterwards throws IOException: Stream closed, proving it was closed. (In real code, the rule of thumb is that whoever opens a resource closes it; a method closing a reader it received as a parameter should be clearly documented.)

The variable must not be reassigned:

import java.io.*;

public class NotFinal {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new StringReader("a"));
        reader = new BufferedReader(new StringReader("b"));
        try (reader) {
            System.out.println(reader.readLine());
        }
    }
}

Compiler output:

NotFinal.java:7: error: variable reader used as a try-with-resources resource neither final nor effectively final
        try (reader) {
             ^
1 error

If the variable could change, it would be unclear which object should be closed — and the first reader here would already have leaked.

null Resources

A resource that is null is allowed; Java simply skips calling close() on it:

public class NullRes {
    static AutoCloseable open(boolean available) {
        return available ? () -> System.out.println("closed") : null;
    }

    public static void main(String[] args) throws Exception {
        try (AutoCloseable r = open(false)) {
            System.out.println("resource is " + r);
        }
        System.out.println("no NullPointerException");
    }
}

Output:

resource is null
no NullPointerException

AutoCloseable has a single abstract method, so a lambda can implement it, as open does here. The generated code checks for null before calling close(). Using a null resource inside the block would still throw, of course; this rule only makes the automatic cleanup safe.

Practical Example: Cleaning a CSV File

This method reads a raw contacts export, writes a cleaned copy, and skips invalid rows. It uses two resources at once — a reader for the input and a writer for the output — both of which must be closed even if processing fails halfway.

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;

public class Clean {
    record Stats(int written, int skipped) { }

    static Stats cleanContacts(Path in, Path out) throws IOException {
        int written = 0, skipped = 0;
        try (BufferedReader reader = Files.newBufferedReader(in, StandardCharsets.UTF_8);
             BufferedWriter writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) {
            writer.write("name,email");
            writer.newLine();
            String line = reader.readLine();           // skip the input header
            while ((line = reader.readLine()) != null) {
                String[] f = line.split(",", -1);
                if (f.length != 2 || !f[1].contains("@")) {
                    skipped++;
                    continue;
                }
                writer.write(f[0].trim() + "," + f[1].trim().toLowerCase());
                writer.newLine();
                written++;
            }
        }
        return new Stats(written, skipped);
    }

    public static void main(String[] args) throws IOException {
        Path dir = Files.createTempDirectory("contacts");
        Path in = Files.writeString(dir.resolve("raw.csv"),
            "Name,Email\n Asha Rao , ASHA@Example.com\nRavi,ravi-at-example.com\nMeena,meena@gocourse.in\nbroken line\n");
        Path out = dir.resolve("clean.csv");

        Stats s = cleanContacts(in, out);
        System.out.println(s);
        System.out.print(Files.readString(out));

        try {
            cleanContacts(dir.resolve("missing.csv"), dir.resolve("x.csv"));
        } catch (NoSuchFileException e) {
            System.out.println("missing input: " + Path.of(e.getFile()).getFileName()
                + ", output created? " + Files.exists(dir.resolve("x.csv")));
        }
    }
}

Output:

Stats[written=2, skipped=2]
name,email
Asha Rao,asha@example.com
Meena,meena@gocourse.in
missing input: missing.csv, output created? false

How it works:

  1. Two resources in one header. The reader is opened first, then the writer. At the end they're closed in reverse: the writer first, which also flushes its buffer to disk, then the reader. Forgetting to close a BufferedWriter is a classic bug that produces empty or truncated output files; try-with-resources makes it impossible.
  2. Processing. The first readLine() consumes the header. split(",", -1) keeps trailing empty fields, so the row count check is reliable. Rows without exactly two fields or without an @ are skipped; valid ones are trimmed and the email lowercased.
  3. The result is returned after the try. By the time Stats is created, both files are closed and the output is complete on disk, so the caller can safely read it immediately — as main does.
  4. Failure on the first resource. For missing.csv, Files.newBufferedReader throws NoSuchFileException. Because resources are initialised left to right, the writer is never created — so no empty x.csv is left behind — and the exception propagates to the caller through the throws IOException clause.
  5. No catch in cleanContacts. try-with-resources doesn't require catch or finally. This method can't decide what a missing file means for the application, so it lets the exception propagate and only guarantees cleanup.

The same structure is used for database code, where all three JDBC objects are resources:

try (Connection con = dataSource.getConnection();
     PreparedStatement ps = con.prepareStatement("SELECT name FROM customers WHERE city = ?")) {
    ps.setString(1, "Chennai");
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getString("name"));
        }
    }
}

Here closing the Connection returns it to the connection pool. Leaking connections is one of the most common causes of applications that slow down and then hang under load.

Writing Your Own Resource Class

Any class that owns something needing cleanup should implement AutoCloseable (or Closeable for I/O-related classes), so that callers can use it with try-with-resources.

import java.io.IOException;
import java.nio.file.*;
import java.util.Comparator;
import java.util.stream.Stream;

public class TempWorkspace implements AutoCloseable {
    private final Path dir;

    public TempWorkspace() throws IOException {
        this.dir = Files.createTempDirectory("job");
    }

    public Path dir() { return dir; }

    @Override
    public void close() throws IOException {
        try (Stream<Path> paths = Files.walk(dir)) {
            paths.sorted(Comparator.reverseOrder())   // delete children before their folder
                 .forEach(p -> p.toFile().delete());
        }
    }

    public static void main(String[] args) throws IOException {
        Path d;
        try (TempWorkspace ws = new TempWorkspace()) {
            d = ws.dir();
            Files.createDirectories(d.resolve("a/b"));
            Files.writeString(d.resolve("a/b/report.txt"), "x");
        }
        System.out.println("exists after close: " + Files.exists(d));
    }
}

Output:

exists after close: false

The workspace creates a temporary folder in its constructor; the job writes files into nested subfolders; and close() deletes everything when the try block ends. Sorting the walked paths in reverse order puts deeper paths first, so files and subfolders are deleted before the folders that contain them (a non-empty folder can't be deleted).

Guidelines for close():

  • Declare the narrowest exception possible. If closing can't fail, declare no exception at all, so callers don't need a catch.
  • Make it idempotent. Calling close() twice should be harmless. Closeable requires this; AutoCloseable strongly recommends it.
  • Don't throw InterruptedException from close(). If it were suppressed, the thread's interrupt request could be lost; the AutoCloseable documentation specifically warns against it.
  • Release everything you own, even if one step fails — for example, by using a nested try-with-resources as above (Files.walk returns a Stream that holds an open directory handle, so it is a resource too).

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Java try-with-resources and get answers drawn from it.

Signed-in readers only.