Exception Handling

Course 1 · Ch 7
Exception Handling
A real middle ground between C++'s unenforced exceptions and Rust's Result<T, E>

Every prior chapter assumed things go right. This one covers what Java does when they don't — and a genuinely distinct position between two approaches already covered on this site.

try/catch/finally — The Basics

try { int[] arr = new int[3]; System.out.println(arr[5]); } catch (ArrayIndexOutOfBoundsException e) { System.out.println("Bad index: " + e.getMessage()); } finally { System.out.println("Always runs, exception or not"); }

Code that might fail goes in try; a specific exception type is handled in catch; finally runs unconditionally, whether an exception was thrown or not.

The Exception Hierarchy

Every exception is a Throwable. Below that: Error (serious JVM-level problems, not meant to be caught) and Exception, which splits again into RuntimeException and everything else — that split is exactly where checked and unchecked diverge.

Checked Exceptions — Compiler-Enforced Acknowledgment

public void readFile(String path) throws IOException { // declared — the compiler requires one of these two options Files.readAllBytes(Path.of(path)); } public void readFileSafely(String path) { try { Files.readAllBytes(Path.of(path)); } catch (IOException e) { // or caught right here System.out.println("Failed: " + e.getMessage()); } }

A checked exception — anything that's an Exception but not a RuntimeException, like IOException — must be either caught or declared with throws in the method signature. The compiler enforces this and refuses to compile otherwise. This is genuinely new territory versus C++: cpp2-6's own C++ exceptions carry no compiler-enforced acknowledgment requirement at all — a function that throws can be called with zero handling and zero declaration, compiling cleanly, only to crash at runtime if the exception is never caught anywhere up the call stack.

Unchecked Exceptions

RuntimeException and its subclasses — NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException — carry no such requirement. They can be thrown and left completely unhandled, compiling fine, just like every exception in cpp2-6's C++ model. Unchecked exceptions are typically reserved for programming errors — bugs — rather than expected, recoverable failure conditions.

The Real Middle Ground vs. Rust's Result<T, E>

Rust encodes failure directly in a function's return type — the signature itself says "this can fail," and the caller must handle the Result via match, ?, or an explicit unwrap() or the code won't compile clean. Java's checked exceptions force a genuinely comparable acknowledgment, but through a side-channel — the throws clause — rather than the return type itself. It's a real middle position: more compiler enforcement than C++ ever offers, but less structurally integrated into the type system than Rust's approach.

try-with-resources

try (BufferedReader reader = new BufferedReader(new FileReader(path))) { System.out.println(reader.readLine()); } // reader.close() is called automatically here, even if an exception was thrown above

Any resource implementing AutoCloseable can be declared in the try(...) parentheses — its close() is called automatically once the block exits, exception or not. It's Java's own answer to the resource-cleanup problem cpp1-5's RAII solves via destructors and Rust's Drop solves automatically — genuinely less automatic than either, since it still requires the explicit try(...) syntax rather than firing on scope exit alone.

ApproachC++ (cpp2-6)Java checkedJava uncheckedRust
Compiler-enforced handlingnoyes — catch or declarenoyes — built into the return type
Where the requirement livesnowherethrows clause (side-channel)nowherethe function signature itself
Resource cleanupRAII — automatic on scope exittry-with-resources — automatic within the block, syntax requiredDrop — automatic on scope exit
Catch specific types, not Exception broadly
Catching the broad Exception type hides which failures a piece of code actually expects and silently swallows unrelated bugs along with it — catch the narrowest type that's genuinely recoverable.
An empty catch block hides real bugs
catch (Exception e) {} compiles fine and looks like handling — but it silently discards every failure, including ones that indicate a genuine bug elsewhere. At minimum, log or rethrow; never swallow silently.

Coding Challenges

Challenge 1

Write a method that deliberately divides by zero inside a try block, catch ArithmeticException specifically, and use a finally block to print a message that always runs regardless.

📄 View solution
Challenge 2

Write a method that calls Files.readAllBytes without catching IOException and without declaring throws. Show the resulting compile error, then fix it using throws in the method signature.

📄 View solution
Challenge 3

Write a class implementing AutoCloseable with a close() method that prints a message, then use it in a try-with-resources block alongside code that throws an exception, showing that close() still runs.

📄 View solution

Chapter 7 Quick Reference

  • try/catch/finally — finally always runs, exception or not
  • Checked exceptions (Exception minus RuntimeException) must be caught or declared with throws — compiler-enforced, unlike C++'s cpp2-6 model
  • Unchecked exceptions (RuntimeException and subclasses) carry no such requirement — same as C++'s exceptions
  • Java's checked exceptions are a real middle ground: more enforcement than C++, less structurally integrated than Rust's Result<T, E> in the return type itself
  • try-with-resources auto-closes any AutoCloseable — Java's own, less-automatic answer to RAII/Drop
  • Next chapter: the Collections Framework — Java's own answer to cpp2-2's STL containers