Skip to main content

8.4 try-with-resources and Advanced Exception Techniques

Learn try-with-resources, a core Java 7 feature that eliminates the hassle and mistakes that arise when resources must always be closed after use.

1. The Problem with Resource Management​

Files, database connections, and network sockets must be closed after use. The old approach always required closing in finally and was easy to get wrong.

// ❌ Old style - verbose and error-prone
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("data.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) { // null check required
try {
reader.close(); // close() itself can throw an exception!
} catch (IOException e) {
e.printStackTrace();
}
}
}

2. try-with-resources (Java 7+)​

Declare resources in try ( ... ) parentheses and close() is called automatically when the try block exits — whether it ended normally or via an exception.

Requirement: The resource class must implement java.lang.AutoCloseable (or java.io.Closeable).

// ✅ try-with-resources - concise and safe!
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("File read error: " + e.getMessage());
}
// reader.close() is called automatically! No finally block needed.

Managing Multiple Resources​

// Declare multiple resources with semicolons (closed in reverse order: out → in)
try (
FileInputStream in = new FileInputStream("source.txt");
FileOutputStream out = new FileOutputStream("dest.txt")
) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("File copy complete!");
} catch (IOException e) {
e.printStackTrace();
}

Custom AutoCloseable Implementation​

class DatabaseConnection implements AutoCloseable {
private final String url;

DatabaseConnection(String url) {
this.url = url;
System.out.println("DB connected: " + url);
}

void query(String sql) {
System.out.println("Query executed: " + sql);
}

@Override
public void close() {
System.out.println("DB connection closed: " + url); // called automatically
}
}

// Usage
try (DatabaseConnection conn = new DatabaseConnection("jdbc:mysql://localhost/shop")) {
conn.query("SELECT * FROM users");
} // conn.close() called automatically here
// Output:
// DB connected: jdbc:mysql://localhost/shop
// Query executed: SELECT * FROM users
// DB connection closed: jdbc:mysql://localhost/shop

3. Multi-catch: Combining Multiple Exceptions​

In Java 7+, the | operator lets you handle multiple exceptions in a single catch block.

// ❌ Old style: repeated catch blocks with identical handling
try {
// ...
} catch (FileNotFoundException e) {
System.out.println("Error: " + e.getMessage());
e.printStackTrace();
} catch (ParseException e) {
System.out.println("Error: " + e.getMessage());
e.printStackTrace();
}

// ✅ Multi-catch: combine with |
try {
String data = readFile("config.txt");
int value = Integer.parseInt(data);
} catch (IOException | NumberFormatException e) {
// Handle both exceptions together
System.out.println("Data processing error: " + e.getMessage());
e.printStackTrace();
}

Note: In multi-catch, e is final and cannot be reassigned.


4. Exception Chaining​

When catching an exception and wrapping it in another, preserve the original cause. The root cause can be traced through the stack trace.

class DataService {
void loadData(String path) throws DataLoadException {
try {
// Attempt actual file reading
new FileReader(path);
} catch (FileNotFoundException e) {
// Wrap the low-level exception in a high-level exception (preserving cause)
throw new DataLoadException("Data file not found: " + path, e);
}
}
}

class DataLoadException extends Exception {
DataLoadException(String message, Throwable cause) {
super(message, cause); // cause: the original exception is passed along
}
}

// Usage
try {
new DataService().loadData("missing.csv");
} catch (DataLoadException e) {
System.out.println("High-level error: " + e.getMessage());
System.out.println("Cause: " + e.getCause().getMessage()); // FileNotFoundException
e.printStackTrace(); // Prints the full exception chain
}

5. Checked vs Unchecked Exception Summary​

CategoryExamplesHandling
Checked ExceptionIOException, SQLExceptionMust use try-catch or declare throws
Unchecked ExceptionNullPointerException, IllegalArgumentExceptionOptional (runtime exception)
ErrorOutOfMemoryError, StackOverflowErrorGenerally no handling needed
// Checked Exception: compiler forces handling
void readFile(String path) throws IOException { // throws declaration required
new FileReader(path);
}

// Unchecked Exception: handling is optional (defensive programming recommended)
void divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("Divisor cannot be zero.");
return a / b;
}

Pro Tip

Real-world exception handling patterns:

  1. Checked → Unchecked conversion: Wrapping a library method's Checked exception in an Unchecked exception at the API boundary means callers are not forced to write try-catch everywhere, keeping code cleaner.

    // Wrap Checked as RuntimeException
    static String readFileSafely(String path) {
    try { return Files.readString(Path.of(path)); }
    catch (IOException e) { throw new RuntimeException("File read failed: " + path, e); }
    }
  2. Include context in exception messages: "File not found" is far less useful for debugging than "Profile image for user ID 123 (/uploads/123.jpg) not found."

  3. Abstract try-with-resources with a lambda(helper pattern):

    @FunctionalInterface
    interface ThrowingSupplier<T> {
    T get() throws Exception;
    }

    static <T> T withDb(String url, ThrowingSupplier<T> action) {
    try (var conn = new DatabaseConnection(url)) {
    return action.get();
    } catch (Exception e) {
    throw new RuntimeException(e);
    }
    }

    // Usage
    String result = withDb("jdbc:...", () -> {
    // DB operations
    return "result";
    });