10.3 Try-With-Resources, `AutoCloseable`, Suppressed Exceptions, and Custom Exceptions
Key Takeaways
Try-with-resources automates resource management, guaranteeing that any resource implementing java.lang.AutoCloseable or java.io.Closeable is automatically closed at block exit without requiring manual finally logic.
Multiple resources declared in the resource header are closed in reverse order of declaration (LIFO), and all resources are closed before any explicit catch or finally blocks attached to that try statement execute.
AutoCloseable.close() declares throws Exception, whereas Closeable.close() declares throws IOException; implementations are encouraged to be idempotent so multiple close() invocations cause no harmful side effects.
Java 9+ supports using preexisting final or effectively final resource variables directly inside the try-with-resources header without declaring new local variables.
If both the try block and automatic close() invocations throw exceptions, the exceptions from close() are suppressed and attached to the primary exception via addSuppressed(), retrievable using getSuppressed().
10.3 Try-With-Resources, AutoCloseable, Suppressed Exceptions, and Custom Exceptions
Modern Java applications manage external system resources—such as file handles, database connections, sockets, and streams—using the try-with-resources statement. Introduced in Java 7 and refined in Java 9, try-with-resources prevents resource leaks, provides deterministic closing semantics, manages suppressed exceptions, and simplifies clean object-oriented architecture.
Automatic Resource Management with Try-With-Resources
In classical Java code, releasing resources in finally blocks required nested try-catch structures and defensive null-checking:
// CLASSICAL ANTI-PATTERN (PRE-JAVA 7): Verbose and prone to resource masking
FileInputStream fis = null;
try {
fis = new FileInputStream("app.log");
fis.read();
} catch (IOException e) {
// Handle read error
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) { /* close exception masks read exception! */ }
}
}
The try-with-resources statement replaces this cumbersome pattern with automatic, compiler-generated cleanup. Any object whose type implements java.lang.AutoCloseable can be instantiated or referenced within the try specification header:
try (FileInputStream fis = new FileInputStream("app.log")) {
int data = fis.read();
} catch (IOException e) {
System.err.println("I/O failed: " + e.getMessage());
}
Try-With-Resources Syntax Rules
- Header Parentheses: Resource declarations are enclosed within parentheses immediately following the
trykeyword:try (Resource r = new Resource()) { ... }. - Optional Catch and Finally: Unlike a standard
tryblock, a try-with-resources statement does not require an explicitcatchorfinallyblock. A try-with-resources block with neithercatchnorfinallyis completely valid:// Valid: Automatically closes resource without explicit catch or finally try (var fis = new FileInputStream("data.bin")) { fis.read(); } - Multiple Resources: Multiple resources are separated by semicolons (
;). The trailing semicolon after the final resource is optional:try (Reader in = new FileReader("in.txt"); Writer out = new FileWriter("out.txt")) { out.write(in.read()); }
AutoCloseable vs. Closeable Interfaces
The try-with-resources mechanism is powered by two foundational interfaces in the Java platform:
java.lang.AutoCloseable (Java 7)
void close() throws Exception;
^
| extends
java.io.Closeable (Java 5)
void close() throws IOException;
Key Differences Between Interfaces
| Feature | java.lang.AutoCloseable | java.io.Closeable |
|---|---|---|
| Package | java.lang (no import required) | java.io (requires import) |
| Introduced | Java 7 | Java 5 (retrofitted in Java 7 to extend AutoCloseable) |
close() Signature | void close() throws Exception; | void close() throws IOException; |
| Exception Hierarchy | Declares broad throws Exception | Declares narrower throws IOException |
| Idempotence Contract | Recommended, but not strictly mandated by specification | Strictly required: calling close() multiple times must have no effect |
Overriding close() Without Checked Exceptions
Because Java permits overriding methods to declare fewer or narrower checked exceptions, an AutoCloseable implementation can declare that its close() method throws a specific subclass (e.g. IOException), an unchecked exception, or no exception at all:
public class MemoryBuffer implements AutoCloseable {
@Override
public void close() {
// No throws clause!
System.out.println("Buffer cleared");
}
}
When using MemoryBuffer in a try-with-resources statement, the compiler does not require catching or declaring Exception, because the concrete close() method throws no checked exceptions!
Resource Closing Order and Execution Order with Catch/Finally
Exam questions frequently test the exact sequence in which resources are closed and when associated catch and finally blocks execute.
Two inviolable rules govern this lifecycle:
- Reverse Declaration Order (LIFO): Resources declared in the
tryheader are closed in the exact opposite order of their declaration. The resource created last is closed first. - Resources Close BEFORE Catch and Finally: Automatic calls to
close()occur immediately upon exiting the protectedtryblock body, BEFORE any explicitcatchorfinallyblocks associated with the try statement execute!
class Res implements AutoCloseable {
private final String id;
public Res(String id) { this.id = id; System.out.print("Init:" + id + " "); }
public void close() { System.out.print("Close:" + id + " "); }
}
public class LifecycleTrace {
public static void main(String[] args) {
try (Res r1 = new Res("1"); Res r2 = new Res("2")) {
System.out.print("Body ");
throw new RuntimeException("Error");
} catch (Exception e) {
System.out.print("Catch ");
} finally {
System.out.print("Finally");
}
}
}
Step-by-step execution trace:
r1initialized: printsInit:1r2initialized: printsInit:2- Try body executes: prints
Body - RuntimeException thrown: resources close in reverse order:
r2.close()executes: printsClose:2r1.close()executes: printsClose:1
- Explicit
catchblock catches the exception: printsCatch - Explicit
finallyblock executes: printsFinally
Final output:
Init:1 Init:2 Body Close:2 Close:1 Catch Finally
Warning
Catch and Finally Blocks See Closed Resources
Because resources are closed before explicit catch and finally blocks run, any attempt to access those resources inside a catch or finally block operates on an already-closed resource!
Java 9+ Effectively Final Resource References
In Java 7 and 8, every resource managed by a try-with-resources statement had to be declared as a new variable inside the try parentheses. Java 9 introduced the ability to use pre-existing variable references, provided they are either explicitly declared final or are effectively final:
public void process(BufferedReader reader) throws IOException {
// Java 9+ syntax: pass preexisting variable directly into try header
try (reader) {
System.out.println(reader.readLine());
}
// reader is closed here
}
The Effectively Final Constraint
A variable is effectively final if its reference is never modified after assignment. If the code reassigns the variable anywhere in the method, the compiler rejects the try-with-resources statement:
BufferedReader r1 = new BufferedReader(new FileReader("a.txt"));
// try (r1) { /* ... */ } // COMPILE ERROR if r1 is reassigned anywhere below!
r1 = new BufferedReader(new FileReader("b.txt")); // Reassignment breaks effectively final status
Suppressed Exceptions Architecture
In classical exception handling, if both the try block and the finally block threw exceptions, the exception thrown in finally completely masked and destroyed the original exception.
Try-with-resources solves this via the suppressed exceptions mechanism:
- Primary Exception: An exception thrown from the body of the
tryblock is recognized as the primary exception. - Suppression During Close: If one or more resources throw exceptions while being automatically closed, those secondary exceptions do not mask the primary exception. Instead, the runtime catches each closing exception and attaches it to the primary exception using
Throwable.addSuppressed(Throwable exception). - Retrieval: The caller or catching block receives the primary exception. The attached suppressed exceptions can be inspected by invoking
Throwable.getSuppressed(), which returns an array ofThrowable[]. - Normal Completion with Failing Close: If the
trybody completes normally without throwing an exception, but a resource'sclose()method throws an exception, thatclose()exception becomes the primary exception thrown to the caller.
class Faulty implements AutoCloseable {
private final String id;
public Faulty(String id) { this.id = id; }
public void close() throws Exception {
throw new Exception("Close failed for " + id);
}
}
public class SuppressedDemo {
public static void main(String[] args) {
try (Faulty f1 = new Faulty("F1"); Faulty f2 = new Faulty("F2")) {
throw new IllegalStateException("Primary computation failed");
} catch (Exception e) {
System.out.println("Caught: " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println(" Suppressed: " + s.getMessage());
}
}
}
}
Output:
Caught: Primary computation failed
Suppressed: Close failed for F2
Suppressed: Close failed for F1
Notice that the suppressed exceptions appear in the reverse closing order (F2, then F1).
Designing Custom Exceptions
When building enterprise applications, developers define domain-specific exceptions to communicate business rule violations.
Checked vs. Unchecked Custom Exceptions
- Extend
java.lang.Exception: To create a checked exception for recoverable business scenarios where callers must provide an alternative fallback. - Extend
java.lang.RuntimeException: To create an unchecked exception for unrecoverable state violations, precondition breaches, or programming errors.
The Four Standard Constructor Idioms
When creating custom exceptions, follow the standard library pattern by implementing the four canonical constructors from Throwable:
public class PaymentProcessingException extends Exception {
// 1. Default no-arg constructor
public PaymentProcessingException() {
super();
}
// 2. Message-only constructor
public PaymentProcessingException(String message) {
super(message);
}
// 3. Cause-only constructor (exception chaining)
public PaymentProcessingException(Throwable cause) {
super(cause);
}
// 4. Message and cause constructor
public PaymentProcessingException(String message, Throwable cause) {
super(message, cause);
}
}
Passing the cause parameter to super(cause) preserves the original root exception (accessible via getCause()), ensuring full stack trace traceability across architectural layers.
Consider the following program:
class Door implements AutoCloseable {
private final String name;
public Door(String name) { this.name = name; }
public void open() { System.out.print("O:" + name + " "); }
public void close() { System.out.print("C:" + name + " "); }
}
public class TryOrderDemo {
public static void main(String[] args) {
try (Door d1 = new Door("1"); Door d2 = new Door("2")) {
d1.open();
d2.open();
throw new RuntimeException("Fail");
} catch (Exception e) {
System.out.print("Catch ");
} finally {
System.out.print("Finally");
}
}
}
What is the exact output when this program is executed?
O:1 O:2 C:1 C:2 Catch Finally
O:1 O:2 Catch Finally C:2 C:1
O:1 O:2 C:2 C:1 Catch Finally
O:1 O:2 Catch C:2 C:1 Finally
Consider the following program:
class FaultyResource implements AutoCloseable {
public void close() throws Exception {
throw new IllegalStateException("Close failed");
}
}
public class SuppressedDemo {
public static void main(String[] args) {
try (FaultyResource r = new FaultyResource()) {
throw new IllegalArgumentException("Primary failure");
} catch (Exception e) {
System.out.println(e.getMessage() + " | Suppressed: " + e.getSuppressed().length);
}
}
}
What does this program output when executed?
Primary failure | Suppressed: 1
Close failed | Suppressed: 1
Close failed | Suppressed: 0
Primary failure | Suppressed: 0
Consider the following code snippet using Java 17:
class Connection implements AutoCloseable {
public void close() {}
}
public class ResourceTest {
public void connect() {
Connection conn = new Connection();
try (conn) {
System.out.println("Connected");
}
conn = new Connection();
}
}
What is the result of attempting to compile this code?
Compilation fails because variables in try-with-resources must be explicitly declared inside the parentheses.
Compiles successfully and prints: Connected
Compilation fails at try (conn) because conn is not effectively final.
Compilation fails because Connection must implement java.io.Closeable instead of AutoCloseable.
Consider the following custom resource class and client code:
class CleanBuffer implements AutoCloseable {
@Override
public void close() {
System.out.println("Cleaned");
}
}
public class Client {
public static void main(String[] args) {
try (CleanBuffer cb = new CleanBuffer()) {
System.out.println("Working");
}
}
}
What is the result of compiling and running this code?
Compilation fails because CleanBuffer.close() must declare throws Exception as specified in AutoCloseable.
Compiles, but throws an UnsupportedOperationException at runtime.
Compilation fails because the main method does not declare or catch java.lang.Exception.
Compiles successfully and outputs: Working Cleaned
Sections you finish are checked off in the contents.