Lab 10: Exception Handling

Objective

Handle checked and unchecked exceptions, create custom exception hierarchies, use try-with-resources, apply exception chaining, and design robust error handling strategies.

Background

Java's exception system is one of the most explicit in any language — checked exceptions force you to acknowledge potential failures at compile time. Understanding when to use checked vs unchecked exceptions, how to chain them for debugging, and how to design exception hierarchies is essential for writing resilient Java applications.

Time

40 minutes

Prerequisites

  • Lab 06 (OOP — Classes)

  • Lab 08 (Interfaces — AutoCloseable)

Tools

  • Java 21 (Eclipse Temurin)

  • Docker image: innozverse-java:latest


Lab Instructions

Step 1: try-catch-finally Basics

💡 finally always executes — even if the catch block throws, or return is called inside try. It's the right place to release resources (close files, release locks). However, try-with-resources is preferred for AutoCloseable resources.

📸 Verified Output:


Step 2: Checked vs Unchecked Exceptions

💡 Checked exceptions (IOException, SQLException) force callers to handle or declare them — good for recoverable conditions. Unchecked (RuntimeException subclasses) don't require explicit handling — good for programming errors. Modern Java style leans toward unchecked; checked exceptions can make APIs verbose.

📸 Verified Output:


Step 3: Custom Exception Hierarchy

💡 Exception hierarchies let callers catch at the right granularity. A REST controller might catch ValidationException to return HTTP 400, NotFoundException for HTTP 404, and AppException as a fallback for HTTP 500. The code field maps directly to error codes in API responses.

📸 Verified Output:


Step 4: Exception Chaining & Stack Traces

💡 Always chain exceptions when wrapping: throw new HighLevelException("context", e). This preserves the original stack trace while adding context. Without chaining, you lose the root cause — the most important debugging information. Never catch (Exception e) { throw new Exception("error"); } (loses original).

📸 Verified Output:


Step 5: try-with-resources

💡 try-with-resources guarantees close() is called in reverse declaration order, even if an exception occurs in the try block, in another resource's constructor, or in a previous close(). This replaced the verbose and error-prone try/finally pattern for resource cleanup.

📸 Verified Output:


Step 6: Result Type — Exception-Free Error Handling

💡 The Result type avoids exceptions for expected failure cases. Instead of try/catch, you chain .map() and use getOrElse(). This is the pattern from Rust (Result<T,E>), Kotlin (kotlin.Result), and Scala (Try). Use it for operations where failure is a normal business case, not an exceptional condition.

📸 Verified Output:


Step 7: Global Exception Handler

💡 Thread.setDefaultUncaughtExceptionHandler is your last line of defense. In production applications, this handler should log the full stack trace, notify your monitoring system (PagerDuty, Sentry), and potentially restart the thread. Without it, uncaught exceptions silently terminate threads.

📸 Verified Output:


Step 8: Full Example — Robust File Processor

💡 Collect errors instead of failing fast when processing bulk data. Instead of throwing on the first bad record, log the error and continue — then report all failures at the end. This is standard in ETL pipelines, CSV importers, and batch processors where partial success is acceptable.

📸 Verified Output:

📸 Verified Output (corrected):


Verification

Summary

You've covered try-catch-finally, checked vs unchecked exceptions, custom exception hierarchies, exception chaining, try-with-resources, the Result type pattern, global uncaught exception handlers, and robust bulk processing. Good exception handling is what separates production-ready code from prototype code.

Further Reading

Last updated