File I/O & NIO.2

Try-with-Resources for I/O

15 min Lesson 8 of 13

Try-with-Resources for I/O

Every stream, reader, writer, or channel you open is a finite operating-system resource: a file descriptor. If your code throws before it reaches a manual close() call, that descriptor leaks. Leak enough descriptors and the JVM — or the whole OS — starts refusing to open new files. Try-with-resources, introduced in Java 7 and refined in Java 9, is the language mechanism that guarantees close() is always called, no matter what.

The Problem with Manual close()

The naive pattern looks harmless:

BufferedReader reader = new BufferedReader(new FileReader("data.txt")); String line = reader.readLine(); // what if this throws? reader.close(); // never reached on exception

Even wrapping in a try/finally is tedious and error-prone when multiple resources are involved:

BufferedReader reader = null; try { reader = new BufferedReader(new FileReader("data.txt")); String line = reader.readLine(); } finally { if (reader != null) { try { reader.close(); // close() itself can throw — swallows the real exception } catch (IOException ignored) {} } }

Notice the nested try inside finally: if both the body and close() throw, the body's exception is silently discarded. That is exactly the class of hard-to-diagnose bug that try-with-resources was designed to eliminate.

The AutoCloseable Contract

Try-with-resources works with any class that implements java.lang.AutoCloseable (or its sub-interface java.io.Closeable). The single required method is:

public void close() throws Exception;

All standard I/O classes — InputStream, OutputStream, Reader, Writer, RandomAccessFile, NIO.2 Channels, DirectoryStream, SeekableByteChannel — implement AutoCloseable, so they all work in a try-with-resources block automatically.

Basic Syntax

try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } // reader.close() is called here — always, even if an exception occurred

The resource declaration lives inside the parentheses after try. When the block exits — normally, via return, or via any exception — the runtime calls close() on every declared resource in reverse declaration order.

Multiple Resources in One Block

Declare resources separated by semicolons. They are closed in reverse order (last-declared closed first), mirroring how you would manually nest close calls:

Path source = Path.of("input.txt"); Path dest = Path.of("output.txt"); try ( BufferedReader reader = Files.newBufferedReader(source); BufferedWriter writer = Files.newBufferedWriter(dest) ) { String line; while ((line = reader.readLine()) != null) { writer.write(line); writer.newLine(); } } // writer closed first, then reader
Closing order matters for correctness. A BufferedWriter buffers data in memory; closing it flushes the buffer to disk. If the writer were closed after an exception escapes, data could be lost. Try-with-resources guarantees the flush-and-close happens even on failure.

Suppressed Exceptions

This is the subtle, important part. If the body throws exception A, and then close() also throws exception B, Java does NOT discard A. Instead, B is attached to A as a suppressed exception:

try (var stream = new FailingStream()) { stream.doWork(); // throws IOException("body failed") } // close() also throws IOException("close failed") // Caller catches IOException("body failed") // ex.getSuppressed()[0] is IOException("close failed")

You can inspect suppressed exceptions when debugging:

try (var r = Files.newBufferedReader(Path.of("missing.txt"))) { r.readLine(); } catch (IOException ex) { System.err.println("Primary: " + ex.getMessage()); for (Throwable s : ex.getSuppressed()) { System.err.println("Suppressed: " + s.getMessage()); } }
Prefer try-with-resources over try/finally for every AutoCloseable. The suppressed-exception mechanism is more correct than the old pattern where the close-exception silently replaced the body-exception.

Java 9: Effectively Final Variables

Java 9 lets you use an already-declared effectively final variable directly in the resource list — no need to re-declare it:

BufferedReader reader = openReader(); // obtained elsewhere try (reader) { // Java 9+: variable reference, not declaration process(reader); } // reader.close() is still guaranteed

This is handy when the resource is conditionally created before the try block or passed into a method.

Implementing AutoCloseable in Your Own Classes

Any class that wraps an external resource should implement AutoCloseable so callers can use it safely:

public final class CsvWriter implements AutoCloseable { private final BufferedWriter writer; public CsvWriter(Path path) throws IOException { this.writer = Files.newBufferedWriter(path); } public void writeRow(String... columns) throws IOException { writer.write(String.join(",", columns)); writer.newLine(); } @Override public void close() throws IOException { writer.close(); // delegates to the underlying resource } } // usage: try (var csv = new CsvWriter(Path.of("report.csv"))) { csv.writeRow("name", "score"); csv.writeRow("Alice", "98"); }
Do not swallow exceptions in close(). A common mistake is catching and ignoring IOException inside close(). If flushing the buffer fails (disk full, network share disconnected), the caller should know. Only suppress if you are certain it cannot carry meaningful information.

Idempotent close() and close() Called Twice

The contract for Closeable (the I/O sub-interface) explicitly requires that close() be idempotent: calling it a second time must have no effect and must not throw. AutoCloseable does not make that guarantee for non-I/O resources, so if you implement it yourself for a non-I/O context, document whether repeated calls are safe.

NIO.2 Resources Are Also AutoCloseable

NIO.2 types integrate cleanly:

Path dir = Path.of("/tmp/work"); try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) { for (Path entry : stream) { System.out.println(entry.getFileName()); } } try (SeekableByteChannel channel = Files.newByteChannel(Path.of("data.bin"))) { ByteBuffer buf = ByteBuffer.allocate(1024); while (channel.read(buf) > 0) { buf.flip(); // process buffer buf.clear(); } }

Trade-offs and Edge Cases

  • Resource initialisation failure: If the constructor of the second resource throws, the first is still closed. Java initialises and tracks each resource independently.
  • null resources: A null in the resource list is silently skipped — no NPE, no close call. Useful when a resource may not have been created yet, but be cautious: it hides bugs where a resource was never opened.
  • Checked vs unchecked: close() declared as throwing Exception forces the caller to handle it. If your close() can only throw unchecked exceptions (or nothing at all), declare it as throws nothing to remove that burden from callers.

Summary

Try-with-resources is the correct, idiomatic way to manage any AutoCloseable resource in Java. It guarantees close() runs unconditionally, handles suppressed exceptions properly rather than discarding them, and eliminates the boilerplate of nested try/finally blocks. Every I/O class in the standard library implements AutoCloseable, and your own resource-holding classes should too. When you open a file, channel, or stream, always use a try-with-resources block — no exceptions (except the ones Java manages for you).