Exception Handling
Java checked vs unchecked exceptions, try-with-resources vs TS try/catch
Introduction
In this lesson, you'll learn about exception handling in Java. Coming from TypeScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In TypeScript, you're familiar with java checked vs unchecked exceptions, try-with-resources vs ts try/catch.
Java has its own approach to java checked vs unchecked exceptions, try-with-resources vs ts try/catch, which we'll explore step by step.
The Java Way
Let's see how Java handles this concept. Here's a typical example:
import java.io.*;
import java.sql.*;
// Checked exception: MUST handle or declare 'throws'
public String readFile(String path) throws IOException {
// IOException is checked — callers must handle it
return Files.readString(Path.of(path));
}
// Unchecked exception: extends RuntimeException, no declaration needed
public class ValidationException extends RuntimeException {
private final String field;
ValidationException(String field, String message) {
super(message);
this.field = field;
}
public String getField() { return field; }
}
// try-with-resources (Java 7+) — auto-closes
// equivalent to TS try/finally with manual close
public void process(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path));
Connection conn = DriverManager.getConnection(dsn)) {
// reader and conn auto-closed at end of block
String line = reader.readLine();
PreparedStatement stmt = conn.prepareStatement("SELECT ?");
stmt.setString(1, line);
}
// IOException propagated to caller
}
// Catching multiple types
try {
riskyOperation();
} catch (IOException | SQLException e) { // multi-catch
logger.error("IO or SQL error: {}", e.getMessage());
} catch (ValidationException e) { // unchecked
logger.warn("Validation: {} {}", e.getField(), e.getMessage());
} finally {
cleanup();
}Comparing to TypeScript
Here's how you might have written similar code in TypeScript:
// TypeScript: all exceptions are unchecked
try {
const data = await fetchUser(id);
processData(data);
} catch (e) {
if (e instanceof NetworkError) {
console.error("Network:", e.message);
} else if (e instanceof ValidationError) {
console.error("Validation:", e.field, e.message);
} else {
throw e; // re-throw unknown
}
} finally {
releaseResources();
}
// Custom error
class NetworkError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = "NetworkError";
}
}
// No concept of "checked" exceptions
async function readFile(path: string): Promise<string> {
return fs.readFile(path, "utf-8"); // no throws declaration
}You may be used to different syntax or behavior.
Java has checked exceptions (must handle or declare 'throws') — TS has none
You may be used to different syntax or behavior.
Checked exceptions: IOException, SQLException — extend Exception
You may be used to different syntax or behavior.
Unchecked exceptions: NullPointerException, ValidationException — extend RuntimeException
You may be used to different syntax or behavior.
try-with-resources auto-closes Closeable resources (like 'using' in C# or 'with' in Python)
You may be used to different syntax or behavior.
Multi-catch: catch (IOException | SQLException e) handles multiple types
Step-by-Step Breakdown
1. Checked vs Unchecked
Checked exceptions must be declared with 'throws' or caught. Unchecked (RuntimeException) don't need declaration. This is a Java-unique concept.
// TS: no distinction — all exceptions are unchecked
async function readFile(p: string): Promise<string> {}// Checked — callers must handle:
public String readFile(String p) throws IOException { ... }
// Unchecked — no declaration needed:
public void validate(int n) { // throws IllegalArgumentException implicitly
if (n < 0) throw new IllegalArgumentException("negative");
}2. try-with-resources
Resources implementing Closeable/AutoCloseable are automatically closed when the try block exits, even on exception.
const stream = await openStream(path);
try { ... } finally { await stream.close(); }try (InputStream in = new FileInputStream(path)) {
// in.close() called automatically
byte[] data = in.readAllBytes();
} // in.close() called here3. Multi-catch
Java 7+ allows catching multiple exception types in one catch block with |. The exception variable is effectively final.
catch (e) {
if (e instanceof A || e instanceof B) { handle(e); }
}catch (IOException | SQLException e) {
logger.error("error: {}", e.getMessage());
// e is effectively final — cannot reassign
}4. Wrapping Checked Exceptions
When calling checked APIs from unchecked code, wrap them in RuntimeException. This is common in lambda expressions.
// TS: all exceptions propagate without wrappers// Wrap IOException in RuntimeException for use in streams
list.stream().map(path -> {
try { return Files.readString(path); }
catch (IOException e) { throw new UncheckedIOException(e); }
}).collect(toList());Common Mistakes
When coming from TypeScript, developers often make these mistakes:
- Java has checked exceptions (must handle or declare 'throws') — TS has none
- Checked exceptions: IOException, SQLException — extend Exception
- Unchecked exceptions: NullPointerException, ValidationException — extend RuntimeException
Key Takeaways
- Checked exceptions (IOException etc.) must be declared with 'throws' or caught
- Unchecked exceptions (RuntimeException subclasses) need no declaration
- try-with-resources auto-closes any AutoCloseable resource
- Multi-catch: catch (A | B e) handles multiple exception types in one block