API Contracts & ErrorsESSENTIAL7 min deep dive

Clean Exception Handling Patterns

#Exception Handling#Domain Exceptions#ControllerAdvice#Anti-Pattern#Clean Code

The 3-Line Engineering Rule

1. THE RULE

Never catch `Exception` and silently swallow it; translate technical exceptions into rich domain exceptions at integration boundaries.

2. WHY IT MATTERS

Swallowing exceptions (`catch (Exception e) {}`) hides corrupted state, makes bugs impossible to locate, and causes silent data loss.

3. PRODUCTION FIX

Catch specific exceptions, wrap in custom domain exceptions with context, preserve original cause (`throw new DomainEx('failed', cause)`), and handle globally via `@ControllerAdvice`.

Architectural Blueprint & Sequence

Hover over nodes to inspect state transitions
EXCEPTION TRANSLATION LADDERSCRIBBLE BLUEPRINT
Layered exception handling translating low-level driver errors into clean domain exceptions
#1

Low-Level Driver

Throws PSQLException (Deadlock)

#2

Integration Adapter

Catches & translates with cause

#3
Domain Context

Domain Service

Throws LockAcquisitionException

#4
Clean Boundary

Global @ControllerAdvice

Logs traceId & returns HTTP 409

Execution Sequence & Transitions
Low-Level DriverIntegration Adapter[raw driver error]
Integration AdapterDomain Service[wrap with cause]
Domain ServiceGlobal @ControllerAdvice[bubble up]
Global @ControllerAdviceLow-Level Driver[log error with traceId]

Key Engineering Takeaways

  • Never catch `Throwable` or `Exception` without re-throwing or logging the stack trace.
  • Always pass the original cause when wrapping exceptions: `new CustomException(msg, cause)`.
  • Distinguish recoverable business exceptions from fatal system defects.
  • Handle exceptions at architectural boundaries (API Controllers / Messaging Handlers), not in every 3rd line.

The Problem & Production Reality

Exception handling is about managing failure gracefully without obscuring the root cause. Bad exception handling either swallows the error (leaving developers blind) or litters business logic with dozens of try/catch blocks that obscure the core algorithm.

Anti-Pattern: Swallowing Exceptions and Dropping Root Causes

Empty catch blocks or catching an exception and throwing a new one without passing the original cause.

ANTI-PATTERN EXAMPLE (JAVA)
// ❌ ANTI-PATTERN 1: The Silent Killer
try {
    paymentService.charge(user, amount);
} catch (Exception e) {
    // Empty block! Payment failed, but program continues as if it succeeded!
}

// ❌ ANTI-PATTERN 2: Lost Stack Trace Cause
try {
    db.save(order);
} catch (SQLException e) {
    throw new RuntimeException("Database error"); // ❌ Dropped 'e'! Root cause is lost forever!
}

Production Pattern: Domain Hierarchy with Preserved Cause & Global Handler

Create a typed hierarchy of domain exceptions that preserve the underlying cause and map cleanly to HTTP status codes.

PRODUCTION-READY CODE (JAVA)
// ✅ PRODUCTION PATTERN: Domain Exception Hierarchy
public abstract class DomainException extends RuntimeException {
    protected DomainException(String message) { super(message); }
    protected DomainException(String message, Throwable cause) { super(message, cause); }
}

public class OrderNotFoundException extends DomainException {
    public OrderNotFoundException(String orderId) {
        super("Order not found with identifier: " + orderId);
    }
}

public class PaymentGatewayException extends DomainException {
    public PaymentGatewayException(String message, Throwable cause) {
        super(message, cause); // Always preserve cause!
    }
}

// Clean Domain Service (No try-catch clutter)
@Service
public class OrderService {
    public Order getOrder(String id) {
        return orderRepo.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));
    }
}

Trade-off Analysis & Staff Recommendations

AspectPros / StrengthsCons / HazardsStaff Recommendation
Checked vs Unchecked ExceptionsChecked exceptions force callers to consider failure cases at compile time.Checked exceptions pollute method signatures throughout intermediate architectural layers.Prefer unchecked (`RuntimeException`) domain exceptions; handle them centrally at boundaries.
Exceptions for Control FlowNone.Constructing exceptions is slow because the JVM fills in the entire execution stack trace.Never use exceptions for normal control flow (e.g. checking if a user exists); return `Optional<T>`.

PR Code Review Checklist (Click to verify)