Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Spring Boot REST API Development

Spring Boot Basics

  • What is Spring Boot?
  • Dependency Injection
  • Building REST Controllers

Validation & Error Handling

  • Bean Validation with @Valid
  • Global Exception Handling

Spring Framework Internals

  • IoC Container Fundamentals
  • Bean Lifecycle In Detail
  • Component Scanning & Configuration
  • Auto-Configuration Mechanism
  • Spring AOP
  • @Transactional Deep Dive

Microservices Architecture

  • Monolith to Microservices Decomposition
  • API Gateway
  • Service Discovery
  • Inter-Service Communication Choices
  • Event-Driven Architecture Patterns
  • Messaging Technology Choices

Resilience Patterns

  • Configuration Management
  • Circuit Breaker Pattern
  • Retry & Backoff Strategies
  • Bulkhead & Rate Limiting
  • Timeout Strategy
  • Why Microservices Fail

Distributed Data & Consistency Patterns

  • Two-Phase Commit
  • Saga Pattern
  • Outbox Pattern
  • Eventual Consistency Design
  • CQRS Basics
  • CAP Theorem

Observability & Operations

  • Centralized Logging
  • Distributed Tracing
  • Metrics & Monitoring
  • Alerting Strategy
  • Health Checks

Payment Systems

  • Payment — Requirements
  • Payment — Core Flow
  • Payment — Idempotency Implementation
  • Payment — Failure Handling & Reconciliation
  • Payment — Security

Order Management System

  • OMS — Requirements
  • OMS — State Machine Design
Chaturmind
← Spring Boot REST API Development

Spring Boot Basics

  • What is Spring Boot?
  • Dependency Injection
  • Building REST Controllers

Validation & Error Handling

  • Bean Validation with @Valid
  • Global Exception Handling

Spring Framework Internals

  • IoC Container Fundamentals
  • Bean Lifecycle In Detail
  • Component Scanning & Configuration
  • Auto-Configuration Mechanism
  • Spring AOP
  • @Transactional Deep Dive

Microservices Architecture

  • Monolith to Microservices Decomposition
  • API Gateway
  • Service Discovery
  • Inter-Service Communication Choices
  • Event-Driven Architecture Patterns
  • Messaging Technology Choices

Resilience Patterns

  • Configuration Management
  • Circuit Breaker Pattern
  • Retry & Backoff Strategies
  • Bulkhead & Rate Limiting
  • Timeout Strategy
  • Why Microservices Fail

Distributed Data & Consistency Patterns

  • Two-Phase Commit
  • Saga Pattern
  • Outbox Pattern
  • Eventual Consistency Design
  • CQRS Basics
  • CAP Theorem

Observability & Operations

  • Centralized Logging
  • Distributed Tracing
  • Metrics & Monitoring
  • Alerting Strategy
  • Health Checks

Payment Systems

  • Payment — Requirements
  • Payment — Core Flow
  • Payment — Idempotency Implementation
  • Payment — Failure Handling & Reconciliation
  • Payment — Security

Order Management System

  • OMS — Requirements
  • OMS — State Machine Design
HomeLearnSpring BootSpring Boot REST API DevelopmentValidation & Error Handling
✓ FreeIntermediate· 6 min read

Global Exception Handling

@ControllerAdvice and @ExceptionHandler — return consistent error responses.

Published September 21, 2026


Global Exception Handling

When something goes wrong in an API (a resource isn't found, input is invalid, a dependency is down), the client needs two things: the right HTTP status and an error body in a predictable shape it can parse. Without a deliberate strategy, each controller handles errors its own way, and unexpected exceptions leak Spring's default error page, or worse, internal details like SQL or class names.

Global exception handling puts all of that in one place. Controllers and services simply throw, and a single @RestControllerAdvice class decides how each kind of exception becomes an HTTP response.

The pieces

  • @ExceptionHandler(SomeException.class): a method that handles that exception type (and its subclasses). Placed inside one controller, it only covers that controller.
  • @RestControllerAdvice: a class whose @ExceptionHandler methods apply to all controllers. It's @ControllerAdvice + @ResponseBody.
  • @ResponseStatus: sets the status code for a handler method (or directly on an exception class).

A domain exception hierarchy

Start by giving your application its own exceptions, each carrying the meaning of the failure, not an HTTP detail:

public abstract class AppException extends RuntimeException {
    protected AppException(String message) { super(message); }
}

public class ResourceNotFoundException extends AppException {
    public ResourceNotFoundException(String resource, String id) {
        super(resource + " not found: " + id);
    }
}

public class BusinessRuleException extends AppException {       // e.g. "cannot cancel a shipped order"
    public BusinessRuleException(String message) { super(message); }
}

The service layer throws these without knowing anything about HTTP. The mapping to status codes happens in exactly one place.

One handler class for the whole API

Spring Boot 3 supports ProblemDetail, the standard error format from RFC 9457 (formerly RFC 7807). It has fixed fields (type, title, status, detail, instance) and room for your own. Using a standard means clients and tools already know how to read your errors.

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    }

    @ExceptionHandler(BusinessRuleException.class)
    public ProblemDetail handleBusinessRule(BusinessRuleException ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
    }

    // Bean Validation failures on @Valid request bodies
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
        pd.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
                .map(e -> Map.of("field", e.getField(), "message", String.valueOf(e.getDefaultMessage())))
                .toList());
        return pd;
    }

    // Malformed JSON, or a path/query value of the wrong type (e.g. /orders/abc for a numeric id)
    @ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentTypeMismatchException.class})
    public ProblemDetail handleBadInput(Exception ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Malformed request");
    }

    // Safety net: everything else is a bug or an outage — log it fully, reveal nothing
    @ExceptionHandler(Exception.class)
    public ProblemDetail handleUnexpected(Exception ex, HttpServletRequest request) {
        String errorId = UUID.randomUUID().toString();
        log.error("Unhandled error {} on {} {}", errorId, request.getMethod(), request.getRequestURI(), ex);
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(
                HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred");
        pd.setProperty("errorId", errorId);       // the client can quote this; you can find it in the logs
        return pd;
    }
}

A client now receives, for example:

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "Order not found: 9f2c",
  "instance": "/api/v1/orders/9f2c"
}

How Spring picks the handler

When an exception is thrown, Spring looks for the most specific matching handler: an exact type match beats a handler for its superclass. So ResourceNotFoundException goes to handleNotFound, not to the catch-all Exception handler, regardless of the order of the methods in the class. Handlers inside a controller take priority over global advice. If you have several advice classes, @Order decides which is consulted first, and @RestControllerAdvice(basePackages = "...") can limit one to part of the app.

Reusing Spring's built-in handling

Spring MVC already knows how to map its own exceptions: unsupported HTTP method → 405, unsupported media type → 415, missing request parameter → 400. Extending ResponseEntityExceptionHandler gives you all of those in ProblemDetail format. You then add handlers only for your own exceptions:

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    // your @ExceptionHandler methods for domain exceptions go here
}

Setting spring.mvc.problemdetails.enabled=true achieves a similar result without a custom class.

What @ControllerAdvice does not catch

It only sees exceptions thrown inside Spring MVC's handling of a request, from controllers, argument resolution or validation. It does not see:

  • Exceptions in servlet filters, including Spring Security. An unauthenticated request is rejected by the security filter chain before it reaches DispatcherServlet. Customize those responses with an AuthenticationEntryPoint (401) and an AccessDeniedHandler (403).
  • Exceptions in @Async methods or scheduled jobs. There's no HTTP request to answer. Use an AsyncUncaughtExceptionHandler and logging.
  • Errors after the response is committed, for example while streaming a large response.

Logging: signal, not noise

  • 4xx errors are usually the client's fault. Log them at DEBUG or WARN, without stack traces, or your logs fill with "user typed a bad ID".
  • 5xx errors are yours. Log them at ERROR with the stack trace and a correlation ID, and alert on their rate.
  • Never put stack traces, SQL, file paths or class names in the response body. They help attackers and confuse users.

Common mistakes

  • try/catch in every controller method, returning hand-built error maps. The error format drifts, and you lose the single place to change it.
  • Catching and swallowing (catch (Exception e) { return null; }). The client gets a misleading 200 or 404, and the real failure is invisible.
  • Throwing RuntimeException("not found") everywhere, then trying to parse messages in the handler. Use typed exceptions.
  • Letting type-mismatch and parse errors fall through to the catch-all, which turns a client's bad input into a 500. That pollutes your error metrics.

Follow-up questions this topic invites — and their answers

Q: @ControllerAdvice vs @RestControllerAdvice? A: @RestControllerAdvice = @ControllerAdvice + @ResponseBody. The handler methods' return values are written as the response body (JSON). With plain @ControllerAdvice, returned values are treated as view names unless each method adds @ResponseBody. For REST APIs use @RestControllerAdvice.

Q: Should the service layer throw exceptions that know about HTTP status codes? A: Preferably not. Services can be called from message consumers, schedulers or other services where HTTP doesn't exist. Throw exceptions that describe what went wrong in domain terms (ResourceNotFoundException, InsufficientStockException) and map them to statuses in the web layer's advice. ResponseStatusException is fine for quick cases inside controllers themselves.

Q: Why return ProblemDetail instead of your own error class? A: It's an IETF standard (RFC 9457) with a fixed set of fields that clients, API gateways and documentation tools already understand, and it's built into Spring 6 / Boot 3. You can still add your own properties (an errors list, an errorId), so you lose nothing.

Q: How do you customize the response when an unauthenticated user calls a protected endpoint? A: That rejection happens in the Spring Security filter chain, before any controller or @ControllerAdvice runs. Configure an AuthenticationEntryPoint (for 401) and an AccessDeniedHandler (for 403) in your security configuration, and have them write the same ProblemDetail shape so all errors still look alike.

Q: How do you test exception handling? A: With @WebMvcTest and MockMvc: make the mocked service throw the exception, then assert the status and body, for example .andExpect(status().isNotFound()).andExpect(jsonPath("$.detail").value("Order not found: 9f2c")). This checks the mapping end to end without starting the full application.

Previous

Bean Validation with @Valid

Next

IoC Container Fundamentals

AI Tutor

Lesson: Global Exception Handling

Quick actions

AI responses can be inaccurate. Verify critical information.