Lesson 19 โข Advanced
Exception Handling Architecture
Custom exception hierarchies, chained exceptions, and error strategies for large systems.
๐ Before You Start
You should understand:
- Exception Handling (Lesson 12) โ try-catch-finally, throw/throws
- OOP & Inheritance (Lessons 9-10) โ class hierarchies
- Interfaces (Lesson 11) โ contracts and polymorphism
What You'll Learn
- โ Custom exception hierarchies for your domain
- โ Chained exceptions (cause wrapping)
- โ Checked vs unchecked exception strategy
- โ Global exception handlers
- โ Result/Either pattern as an alternative
- โ Enterprise error handling best practices
1๏ธโฃ Designing Exception Hierarchies
๐ก Analogy: Hospital Triage โ A base AppException is the intake form. ValidationException is a minor injury. DatabaseException needs specialist attention. NotFoundException is common and expected. Each type gets routed to the right handler.
Modern rule: Use unchecked (RuntimeException) for most cases. Use checked only when the caller can reasonably recover.
// Domain exception hierarchy
public class AppException extends RuntimeException {
private final String code;
public AppException(String message, String code, Throwable cause) {
super(message, cause);
this.code = code;
}
}
public class ValidationException extends AppException { /* field info */ }
public class NotFoundException extends AppException { /* entity info */ }
public class DatabaseException extends AppException { /* retry info */ }Try It: Custom Exception Hierarchy
Build a domain-specific exception system with error codes and context
// ๐ก Try modifying this code and see what happens!
// Custom Exception Hierarchy (Simulated)
console.log("=== Exception Hierarchy ===\n");
class AppException extends Error {
constructor(message, code, cause) {
super(message);
this.name = "AppException";
this.code = code;
this.cause = cause;
this.timestamp = new Date().toISOString();
}
}
class ValidationException extends AppException {
constructor(field, message) {
super(message, "VAL
...2๏ธโฃ Chained Exceptions
Always preserve the root cause when wrapping exceptions. Pass the original exception as the cause parameter โ this creates a chain that helps debugging.
// โ
Good โ preserves root cause
catch (SQLException e) {
throw new DatabaseException("Failed to fetch user", e);
}
// โ Bad โ loses the stack trace
catch (SQLException e) {
throw new DatabaseException(e.getMessage()); // cause lost!
}3๏ธโฃ The Result Pattern
For expected failures (validation, parsing), consider returning a Result<T> instead of throwing. This makes error handling explicit and avoids the performance cost of stack trace creation.
// Instead of throwing for expected failures:
Result<User> result = userService.findById(id);
if (result.isSuccess()) {
User user = result.getValue();
} else {
String error = result.getError();
}Try It: Exception Chaining & Result Pattern
Practice preserving root causes and using Result instead of throw
// ๐ก Try modifying this code and see what happens!
// Chaining & Result Pattern (Simulated)
console.log("=== Exception Chaining ===\n");
class AppException extends Error {
constructor(message, code, cause) {
super(message); this.code = code; this.cause = cause;
}
}
class DatabaseException extends AppException {
constructor(message, cause) { super(message, "DB_ERROR", cause); }
}
function connectToDatabase() {
throw new Error("Connection refused: port 5432");
}
functio
...4๏ธโฃ Global Exception Handlers
In enterprise apps, you centralize error handling. Spring Boot uses @ControllerAdvice to map exception types to HTTP responses automatically.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
ResponseEntity<?> handleNotFound(NotFoundException e) {
return ResponseEntity.status(404).body(e.getMessage());
}
@ExceptionHandler(ValidationException.class)
ResponseEntity<?> handleValidation(ValidationException e) {
return ResponseEntity.status(400).body(e.getMessage());
}
}Try It: Global Error Handler System
Build a centralized error handler that routes exceptions to the right handler
// ๐ก Try modifying this code and see what happens!
// Global Error Handler (Simulated)
console.log("=== Global Error Handler ===\n");
class AppException extends Error {
constructor(message, code) { super(message); this.code = code; }
}
class ValidationException extends AppException {
constructor(field, message) { super(message, "VALIDATION"); this.field = field; this.name = "ValidationException"; }
}
class NotFoundException extends AppException {
constructor(entity, id) { super(ent
...5๏ธโฃ Common Mistakes
throw new MyEx(e.getMessage()) loses the stack trace. Always pass the original: new MyEx("context", e).catch(Exception e) {} silently swallows errors. At minimum, log the exception.6๏ธโฃ Pro Tips
๐ก Include error codes in exceptions to enable automatic API response mapping.
๐ก Create a base exception per module: PaymentException, OrderException. Callers can catch an entire module's errors.
๐ก In Spring Boot, use @ControllerAdvice with @ExceptionHandler for centralized error handling.
๐ Quick Reference
| Concept | Java Syntax | When to Use |
|---|---|---|
| Custom exception | extends RuntimeException | Domain-specific errors |
| Chained exception | new Ex(msg, cause) | Preserve root cause |
| Checked | extends Exception | Recoverable errors |
| Unchecked | extends RuntimeException | Programming errors |
| Result pattern | Result.ok(v) / .fail(e) | Expected failures |
๐ Lesson Complete!
You can now design robust error handling systems for enterprise Java applications!
Next: Java Generics Advanced โ bounded types, wildcards, and type erasure.
Sign up for free to track which lessons you've completed and get learning reminders.