Skip to content

Error Handling


Definition

Error Handling Definition


Exception Types in Java

Java Exception Hierarchy


Exception Anti-Patterns

// ANTI-PATTERN 1: Catching generic Exception
try {
    processOrder(order);
} catch (Exception e) {
    // Catches everything including bugs!
    log.error("Error", e);
}

// ANTI-PATTERN 2: Empty catch block
try {
    file.close();
} catch (IOException e) {
    // Swallowed! Debugging nightmare
}

// ANTI-PATTERN 3: Catch and rethrow without adding value
try {
    repository.save(order);
} catch (SQLException e) {
    throw e;  // Pointless
}

// ANTI-PATTERN 4: Using exceptions for flow control
try {
    User user = getUser(id);
} catch (UserNotFoundException e) {
    user = createUser(id);  // Expected case treated as exception!
}

// ANTI-PATTERN 5: Logging and throwing
try {
    process();
} catch (ProcessingException e) {
    log.error("Failed", e);  // Logged here
    throw e;                 // And logged again by caller!
}

// ANTI-PATTERN 6: Losing stack trace
try {
    parse(data);
} catch (ParseException e) {
    throw new RuntimeException("Parse failed");  // Lost original cause!
}

Best Practices

// PRACTICE 1: Catch specific exceptions
try {
    saveOrder(order);
} catch (ValidationException e) {
    return Response.badRequest(e.getMessage());
} catch (DuplicateOrderException e) {
    return Response.conflict("Order already exists");
}

// PRACTICE 2: Preserve the cause
try {
    parseConfig(file);
} catch (ParseException e) {
    throw new ConfigurationException("Invalid config file: " + file, e);
    //                                                         ↑ preserve!
}

// PRACTICE 3: Use try-with-resources
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
    // Resources automatically closed
} catch (SQLException e) {
    throw new DataAccessException("Query failed", e);
}

// PRACTICE 4: Return Optional instead of throwing for "not found"
public Optional<User> findById(String id) {
    User user = repository.findById(id);
    return Optional.ofNullable(user);
}

// PRACTICE 5: Create meaningful custom exceptions
public class InsufficientFundsException extends BusinessException {
    private final Money requested;
    private final Money available;

    public InsufficientFundsException(Money requested, Money available) {
        super(String.format(
            "Insufficient funds: requested %s, available %s",
            requested, available));
        this.requested = requested;
        this.available = available;
    }

    // Getters for programmatic access
}

Exception Design

// Designing exception hierarchies

// Base exception for your domain
public abstract class OrderException extends RuntimeException {
    public OrderException(String message) {
        super(message);
    }
    public OrderException(String message, Throwable cause) {
        super(message, cause);
    }
}

// Specific exceptions
public class OrderNotFoundException extends OrderException {
    private final String orderId;

    public OrderNotFoundException(String orderId) {
        super("Order not found: " + orderId);
        this.orderId = orderId;
    }

    public String getOrderId() { return orderId; }
}

public class InvalidOrderStateException extends OrderException {
    private final OrderStatus currentStatus;
    private final OrderStatus requiredStatus;

    public InvalidOrderStateException(OrderStatus current, OrderStatus required) {
        super(String.format(
            "Invalid order state: current=%s, required=%s",
            current, required));
        this.currentStatus = current;
        this.requiredStatus = required;
    }
}

// Checked vs Unchecked decision:
// - Caller can reasonably handle it? → Checked
// - Programming error? → Unchecked
// - Modern preference: Unchecked for everything

Error Handling Patterns

// Pattern 1: Result type (instead of exceptions)
public class Result<T> {
    private final T value;
    private final Error error;
    private final boolean success;

    public static <T> Result<T> success(T value) {
        return new Result<>(value, null, true);
    }

    public static <T> Result<T> failure(Error error) {
        return new Result<>(null, error, false);
    }
}

public Result<Order> processOrder(OrderRequest request) {
    if (!validator.isValid(request)) {
        return Result.failure(new ValidationError(validator.getErrors()));
    }
    Order order = createOrder(request);
    return Result.success(order);
}

// Pattern 2: Either type
public Either<Error, Order> processOrder(OrderRequest request) {
    return validator.validate(request)
        .flatMap(this::checkInventory)
        .flatMap(this::processPayment)
        .map(this::createOrder);
}

// Pattern 3: Error handler/callback
public interface ErrorHandler {
    void handle(Exception e);
}

public void process(Data data, ErrorHandler onError) {
    try {
        doProcess(data);
    } catch (ProcessingException e) {
        onError.handle(e);
    }
}

API Error Responses

// Consistent error responses for APIs

// Standard error response
public record ErrorResponse(
    String code,
    String message,
    Map<String, String> details,
    Instant timestamp
) {}

// Global exception handler (Spring)
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidation(ValidationException e) {
        return ResponseEntity
            .badRequest()
            .body(new ErrorResponse(
                "VALIDATION_ERROR",
                e.getMessage(),
                e.getFieldErrors(),
                Instant.now()
            ));
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException e) {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse(
                "NOT_FOUND",
                e.getMessage(),
                Map.of("resource", e.getResourceId()),
                Instant.now()
            ));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
        log.error("Unexpected error", e);
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse(
                "INTERNAL_ERROR",
                "An unexpected error occurred",
                Map.of(),
                Instant.now()
            ));
    }
}

Tips & Tricks

Error Handling Tips and Tricks