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
💡
finallyalways executes — even if the catch block throws, orreturnis called inside try. It's the right place to release resources (close files, release locks). However,try-with-resourcesis preferred forAutoCloseableresources.
📸 Verified Output:
Step 2: Checked vs Unchecked Exceptions
💡 Checked exceptions (
IOException,SQLException) force callers to handle or declare them — good for recoverable conditions. Unchecked (RuntimeExceptionsubclasses) 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
ValidationExceptionto return HTTP 400,NotFoundExceptionfor HTTP 404, andAppExceptionas a fallback for HTTP 500. Thecodefield 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. Nevercatch (Exception e) { throw new Exception("error"); }(loses original).
📸 Verified Output:
Step 5: try-with-resources
💡
try-with-resourcesguaranteesclose()is called in reverse declaration order, even if an exception occurs in the try block, in another resource's constructor, or in a previousclose(). This replaced the verbose and error-pronetry/finallypattern 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 usegetOrElse(). 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.setDefaultUncaughtExceptionHandleris 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
