try-catch
The try-catch statement is Java's primary mechanism for handling exceptions. Code that might throw an exception is placed in the try block; one or more catch blocks follow, each specifying the exception type to handle and providing the handling logic. When an exception is thrown inside the try block, execution of the try block immediately stops and the JVM searches the catch blocks in order for the first one whose type matches the thrown exception. Understanding the control flow precisely — what runs, what stops, and in what order — is essential for writing correct exception handling code. This entry covers try-catch control flow, exception type matching, catching by supertype, variable scope, and the key principles for writing good catch blocks.
try-catch Control Flow
// ── Basic try-catch structure ────────────────────────────────────────
try {
// Guarded region — code that might throw
String text = Files.readString(Path.of("data.txt")); // throws IOException
int value = Integer.parseInt(text.trim()); // throws NumberFormatException
System.out.println("Value: " + value); // runs only if no exception
} catch (IOException e) {
// Runs if IOException thrown anywhere in try block
System.err.println("File error: " + e.getMessage());
}
// Execution continues here after try-catch (whether or not catch ran)
// ── Precise throw point — statements AFTER throw do NOT run ───────────
try {
System.out.println("Before throw");
throw new RuntimeException("test");
// System.out.println("After throw"); // unreachable — never runs
} catch (RuntimeException e) {
System.out.println("Caught: " + e.getMessage());
}
System.out.println("After try-catch");
// Output:
// Before throw
// Caught: test
// After try-catch
// ── Exception variable scoped to catch block ──────────────────────────
try {
riskyOperation();
} catch (IOException e) {
System.out.println(e.getMessage()); // e accessible here
}
// System.out.println(e.getMessage()); // COMPILE ERROR — e out of scope
// ── Accessing try block variables in catch ─────────────────────────────
String filename = null; // declared BEFORE try — accessible in catch
try {
filename = getFilename();
Files.readString(Path.of(filename));
} catch (IOException e) {
System.err.println("Could not read: " + filename); // filename accessible
}Exception Type Matching and Supertype Catching
// ── Supertype catch — catches all subtypes ───────────────────────────
try {
Files.readString(Path.of("missing.txt"));
} catch (IOException e) {
// Catches IOException AND FileNotFoundException AND SocketException
// (FileNotFoundException extends IOException)
System.err.println("I/O error: " + e.getMessage());
}
// ── Ordering: specific before general ────────────────────────────────
try {
Files.readString(Path.of("missing.txt"));
} catch (FileNotFoundException e) {
// Handles the specific case first
System.err.println("File not found: " + e.getMessage());
} catch (IOException e) {
// Handles all other I/O exceptions
System.err.println("I/O error: " + e.getMessage());
}
// ── Compiler detects unreachable catch blocks ─────────────────────────
try {
Files.readString(Path.of("file.txt"));
// } catch (IOException e) {
// ...
// } catch (FileNotFoundException e) { // COMPILE ERROR
// // FileNotFoundException IS-A IOException
// // The IOException block above already catches all FileNotFoundExceptions
// // This block is UNREACHABLE — compiler rejects it
// }
} catch (IOException e) {
System.err.println("error: " + e.getMessage());
}
// ── Anti-patterns ─────────────────────────────────────────────────────
// BAD: catching everything silences unexpected bugs
try {
process();
} catch (Exception e) {
// Catches NullPointerException, IllegalArgumentException, everything
// Code has a bug? Swallowed here and hidden.
}
// WORSE: empty catch block — failure is completely invisible
try {
process();
} catch (Exception e) {
// Silent failure — program continues in unknown state
}
// GOOD: catch specific, handle meaningfully
try {
process();
} catch (InsufficientFundsException e) {
notifyUser("Insufficient funds: " + e.getBalance());
} catch (OrderNotFoundException e) {
return ResponseEntity.notFound().build();
}Writing Effective Catch Blocks
// ── Recovery pattern ─────────────────────────────────────────────────
public String readConfig(String path) {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
// Recovery: return default config instead of propagating
log.warn("Config file not found at {}, using defaults", path);
return DEFAULT_CONFIG;
}
}
// ── Wrap and rethrow (translate to domain exception) ──────────────────
public Order findOrder(Long id) {
try {
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
} catch (DataAccessException e) {
// Translate infrastructure exception to domain exception
// ALWAYS include the cause — preserves debugging information
throw new ServiceException("Failed to load order " + id, e);
}
}
// ── Log at the handling level, not at every level ─────────────────────
// WRONG — log and rethrow at every level creates duplicate log entries
void levelC() throws IOException {
try { doWork(); }
catch (IOException e) { log.error("error", e); throw e; } // logged here
}
void levelB() throws IOException {
try { levelC(); }
catch (IOException e) { log.error("error", e); throw e; } // logged again!
}
void levelA() {
try { levelB(); }
catch (IOException e) { log.error("error", e); } // logged again!!
}
// Result: same error appears 3 times in the log
// CORRECT — let it propagate, log ONCE at the handling level
void levelC_correct() throws IOException { doWork(); } // no logging
void levelB_correct() throws IOException { levelC_correct(); } // no logging
void levelA_correct() {
try { levelB_correct(); }
catch (IOException e) { log.error("IO failure", e); } // logged ONCE
}
// ── Preserve exception information ───────────────────────────────────
// BAD: loses root cause
catch (SQLException e) {
throw new DataException("Database error"); // cause not preserved!
}
// GOOD: chain the cause
catch (SQLException e) {
throw new DataException("Database error: " + e.getMessage(), e); // cause preserved
}