Back to Spring Exception Handling
Q1Question 1 of 1

How does @ControllerAdvice handle exceptions globally?

Key Concepts
  • @RestControllerAdvice is a global exception handler class that catches exceptions from all controllers in the application
  • @ExceptionHandler(XException.class) methods catch specific exception types and return structured error responses with HTTP status codes
  • Controllers have ZERO try-catch blocks because all exceptions propagate to the global handler automatically
  • ResponseStatusException (Spring 5) allows inline exception throwing with HTTP status: throw new ResponseStatusException(NOT_FOUND)
  • Order handlers from most specific to least specific, with a catch-all Exception handler returning 500 for unexpected errors
  • Never expose internal details like SQL errors or stack traces to the API client in error responses

Definition

@RestControllerAdvice with @ExceptionHandler methods provides centralized exception handling for all controllers, returning consistent structured error responses with appropriate HTTP status codes.
  • @RestControllerAdvice (or @ControllerAdvice) is a class-level annotation that acts as a global exception handler for all controllers in the application.
  • @ExceptionHandler(XException.class) on a method catches a specific exception type thrown by any controller and returns a structured error response.
  • @ResponseStatus(HttpStatus.NOT_FOUND) on the handler method sets the HTTP status code.
  • ResponseStatusException (Spring 5) is an inline alternative: throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found").
  • The result: controllers have zero try-catch blocks. All exceptions propagate to the global handler, which returns consistent error responses across the entire API.

Instead of every teller at National Bank of India having their own error procedure, the bank has a central complaints desk (@ControllerAdvice). Any complaint from any counter goes to this desk. The desk handles each type: insufficient funds returns 400, trade not found returns 404, and system error returns 500. Tellers just do their job without worrying about error handling.

Understanding the Concepts

What is @RestControllerAdvice?

@RestControllerAdvice is a class-level annotation that combines @ControllerAdvice and @ResponseBody. It acts as a global exception handler for all @RestController classes in the application.

JAVA
package com.bank.web;

import java.time.Instant;
import java.math.BigDecimal;

// ErrorResponse DTO for consistent API error format
public class ErrorResponse {
    private final String error;
    private final String message;
    private final int status;
    private final Instant timestamp;

    public ErrorResponse(String error, String message, int status) {
        this.error = error;
        this.message = message;
        this.status = status;
        this.timestamp = Instant.now();
    }

    public String getError() { return error; }
    public String getMessage() { return message; }
    public int getStatus() { return status; }
    public Instant getTimestamp() { return timestamp; }

    public static void main(String[] args) {
        ErrorResponse response = new ErrorResponse("NOT_FOUND", "Trade TRD-001 not found", 404);
        System.out.println("Error: " + response.getError());
        System.out.println("Message: " + response.getMessage());
        System.out.println("Status: " + response.getStatus());
    }
}
TEXT
Error: NOT_FOUND
Message: Trade TRD-001 not found
Status: 404

How do you implement a global exception handler?

Create one class annotated with @RestControllerAdvice containing @ExceptionHandler methods for each exception type.

JAVA
package com.bank.web;

// @RestControllerAdvice
// public class GlobalExceptionHandler {
//
//     @ExceptionHandler(TradeNotFoundException.class)
//     @ResponseStatus(HttpStatus.NOT_FOUND)
//     public ErrorResponse handleNotFound(TradeNotFoundException e) {
//         return new ErrorResponse("NOT_FOUND", e.getMessage(), 404);
//     }
//
//     @ExceptionHandler(InsufficientBalanceException.class)
//     @ResponseStatus(HttpStatus.BAD_REQUEST)
//     public ErrorResponse handleInsufficientBalance(InsufficientBalanceException e) {
//         return new ErrorResponse("INSUFFICIENT_BALANCE", e.getMessage(), 400);
//     }
//
//     @ExceptionHandler(IllegalArgumentException.class)
//     @ResponseStatus(HttpStatus.BAD_REQUEST)
//     public ErrorResponse handleBadRequest(IllegalArgumentException e) {
//         return new ErrorResponse("BAD_REQUEST", e.getMessage(), 400);
//     }
//
//     @ExceptionHandler(Exception.class)
//     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
//     public ErrorResponse handleGeneric(Exception e) {
//         log.error("Unhandled exception", e);
//         return new ErrorResponse("INTERNAL_ERROR", "Something went wrong", 500);
//         // Never expose internal details to the client
//     }
// }

public class GlobalHandlerDemo {
    public static void main(String[] args) {
        System.out.println("Global Exception Handler mapping:");
        System.out.println("  TradeNotFoundException        -> 404 NOT_FOUND");
        System.out.println("  InsufficientBalanceException  -> 400 BAD_REQUEST");
        System.out.println("  IllegalArgumentException      -> 400 BAD_REQUEST");
        System.out.println("  Exception (catch-all)         -> 500 INTERNAL_ERROR");
        System.out.println();
        System.out.println("Controllers: ZERO try-catch blocks");
    }
}
TEXT
Global Exception Handler mapping:
  TradeNotFoundException        -> 404 NOT_FOUND
  InsufficientBalanceException  -> 400 BAD_REQUEST
  IllegalArgumentException      -> 400 BAD_REQUEST
  Exception (catch-all)         -> 500 INTERNAL_ERROR

Controllers: ZERO try-catch blocks

How do controllers look with global handling?

Controllers have no try-catch blocks. They call the service, and any exception propagates to the global handler.

JAVA
package com.bank.web;

// @RestController
// @RequestMapping("/api/v1/trades")
// public class TradeController {
//
//     @GetMapping("/{id}")
//     public Trade getTrade(@PathVariable String id) {
//         return tradeService.findById(id);  // ZERO try-catch
//         // TradeNotFoundException -> GlobalHandler -> 404
//     }
//
//     @PostMapping
//     public Trade createTrade(@Valid @RequestBody TradeRequest request) {
//         return tradeService.create(request);  // ZERO try-catch
//         // ValidationException -> GlobalHandler -> 422
//         // InsufficientBalance -> GlobalHandler -> 400
//     }
//
//     @PostMapping("/{id}/settle")
//     public TradeResult settle(@PathVariable String id) {
//         return tradeService.settle(id);  // ZERO try-catch
//         // Any exception -> GlobalHandler -> appropriate HTTP status
//     }
// }

public class CleanControllerDemo {
    public static void main(String[] args) {
        System.out.println("Controller methods: ZERO try-catch");
        System.out.println("  GET /trades/{id}         -> tradeService.findById(id)");
        System.out.println("  POST /trades             -> tradeService.create(request)");
        System.out.println("  POST /trades/{id}/settle -> tradeService.settle(id)");
        System.out.println();
        System.out.println("All exceptions propagate to @ControllerAdvice");
    }
}
TEXT
Controller methods: ZERO try-catch
  GET /trades/{id}         -> tradeService.findById(id)
  POST /trades             -> tradeService.create(request)
  POST /trades/{id}/settle -> tradeService.settle(id)

All exceptions propagate to @ControllerAdvice

What is ResponseStatusException?

ResponseStatusException (Spring 5) is an inline alternative for simple cases where you do not need a custom exception class.

JAVA
package com.bank.demo;

// import org.springframework.http.HttpStatus;
// import org.springframework.web.server.ResponseStatusException;

// @GetMapping("/{id}")
// public Trade getTrade(@PathVariable String id) {
//     return tradeService.findById(id)
//         .orElseThrow(() -> new ResponseStatusException(
//             HttpStatus.NOT_FOUND, "Trade not found: " + id));
// }
// No custom exception class needed. Status + message in one throw.

public class ResponseStatusExceptionDemo {
    public static void main(String[] args) {
        System.out.println("ResponseStatusException usage:");
        System.out.println("  throw new ResponseStatusException(NOT_FOUND, msg)");
        System.out.println("  Quick, inline, no custom class needed");
        System.out.println("  But: less structured than @ExceptionHandler");
    }
}
TEXT
ResponseStatusException usage:
  throw new ResponseStatusException(NOT_FOUND, msg)
  Quick, inline, no custom class needed
  But: less structured than @ExceptionHandler

How does @ExceptionHandler compare to ResponseStatusException?

| Feature | @ExceptionHandler | ResponseStatusException |
|---|---|---|
| Location | Centralized in @ControllerAdvice | Inline in controller/service |
| Response body | Full custom ErrorResponse DTO | Spring's default error body |
| Reusability | One handler for all controllers | Per-throw-site |
| Custom exception class | Required (TradeNotFoundException) | Not required |
| Best for | Production APIs with consistent format | Quick prototypes, simple cases |

What are common mistakes in Spring exception handling?

JAVA
package com.bank.demo;

public class SpringExceptionMistakes {

    // WRONG: try-catch in every controller method
    // @GetMapping("/{id}")
    // public ResponseEntity<Trade> getTrade(@PathVariable String id) {
    //     try {
    //         return ResponseEntity.ok(tradeService.findById(id));
    //     } catch (TradeNotFoundException e) {
    //         return ResponseEntity.notFound().build();
    //     } catch (Exception e) {
    //         return ResponseEntity.internalServerError().build();
    //     }
    // }
    // 200 try-catch blocks across 15 controllers. Inconsistent.

    // CORRECT: let exceptions propagate to @ControllerAdvice

    // WRONG: exposing internal details in error response
    // return new ErrorResponse("ERROR", exception.toString(), 500);
    // Leaks: class names, SQL queries, stack traces

    // CORRECT: generic message for 500 errors
    // return new ErrorResponse("INTERNAL_ERROR", "Something went wrong", 500);

    public static void main(String[] args) {
        System.out.println("Common mistakes:");
        System.out.println("1. try-catch in every controller method");
        System.out.println("2. Exposing SQL errors to API clients");
        System.out.println("3. Missing catch-all handler for Exception.class");
        System.out.println("4. Not logging the full exception in catch-all");
    }
}
TEXT
Common mistakes:
1. try-catch in every controller method
2. Exposing SQL errors to API clients
3. Missing catch-all handler for Exception.class
4. Not logging the full exception in catch-all

What should a senior engineer know?

  • Handler ordering. Spring resolves handlers from most specific to least specific, similar to catch block ordering. TradeNotFoundException before Exception.
  • Problem Details (RFC 7807/9457). Spring 6+ supports ProblemDetail responses for standards-compliant error formats. Use ProblemDetail.forStatusAndDetail() in handlers.
  • @ControllerAdvice(basePackages = "..."). You can scope a handler to specific packages or controller classes. Useful for multi-module applications.
  • @ExceptionHandler priority. If multiple @ControllerAdvice classes handle the same exception, use @Order to control priority.
  • Correlation IDs. Include a request ID (from MDC or Spring Cloud Sleuth) in the ErrorResponse for distributed tracing.

What are common interview follow-up questions?

  • Difference between @ControllerAdvice and @RestControllerAdvice? (@RestControllerAdvice adds @ResponseBody.)
  • Can @ExceptionHandler be in a controller? (Yes, it handles only that controller's exceptions.)
  • How do you handle validation errors? (@ExceptionHandler for MethodArgumentNotValidException.)
  • What HTTP status for validation failure? (400 or 422 depending on convention.)

Visual Overview

DIAGRAM
  Request
    │
    ▼
  Controller.settle(id)
    │
    │  exception thrown
    ▼
  @RestControllerAdvice
  GlobalExceptionHandler
    │
    ├── TradeNotFoundException     ──▶  404 + ErrorResponse
    ├── InsufficientBalance        ──▶  400 + ErrorResponse
    ├── IllegalArgumentException   ──▶  400 + ErrorResponse
    ├── MethodArgumentNotValid     ──▶  422 + ErrorResponse
    └── Exception (catch-all)      ──▶  500 + ErrorResponse
    │
    ▼
  JSON Response to Client
  {
    "error": "NOT_FOUND",
    "message": "Trade TRD-001 not found",
    "status": 404,
    "timestamp": "2024-01-15T10:30:00Z"
  }

Key Takeaway

@RestControllerAdvice with @ExceptionHandler methods is the Spring Boot standard for global exception handling. Each handler catches a specific exception type from any controller and returns a structured ErrorResponse with the correct HTTP status. Controllers have zero try-catch blocks. They call the service, and any exception propagates automatically to the global handler.

At National Bank of India, Pankaj's API uses one GlobalExceptionHandler class that maps TradeNotFoundException to 404, InsufficientBalanceException to 400, IllegalArgumentException to 400, and the catch-all Exception to 500. Before this pattern, there were 200 try-catch blocks across 15 controllers with inconsistent error formats. After: one handler class, consistent JSON responses, and controllers focused purely on business logic.

Best practices: order handlers from most specific to least specific. Log the full stack trace in the catch-all handler with log.error("msg", e). Never expose SQL errors, class names, or stack traces to the API client. Include a correlation ID in the error response for distributed tracing.

Remember one sentence: @RestControllerAdvice centralizes exception handling so controllers have zero try-catch and every error returns a consistent structured response.

---

Share