Skip to content

Spring Boot · Web MVC

Exception Handling in Spring MVC, Explained

5 min read Updated 2026-06-26 Share:

Practice Exception Handling interview questions

Errors are part of your API contract

Clients spend at least as much code handling your error responses as your success ones. An API that returns a 500 with a stack trace for a missing record, or a different error shape from every endpoint, is painful to integrate against. Spring MVC gives you a clean, centralized way to map any exception to a proper status code and a consistent body — once you know the pieces.

@ExceptionHandler: exception in, response out

A method annotated with @ExceptionHandler handles exceptions thrown by handler methods. Inside a controller it's local to that controller:

@ExceptionHandler(OrderNotFoundException.class)
ResponseEntity<ApiError> handle(OrderNotFoundException ex) {
    return ResponseEntity.status(404).body(new ApiError("NOT_FOUND", ex.getMessage()));
}

It can accept the exception (plus the request/response) and return any normal handler type. But repeating it in every controller is exactly the duplication we want to avoid.

@RestControllerAdvice: handle it once, globally

Move those handlers into a @RestControllerAdvice and they apply to every controller:

@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    ResponseEntity<ApiError> notFound(EntityNotFoundException ex) {
        log.warn("Not found: {}", ex.getMessage());
        return ResponseEntity.status(404).body(new ApiError("NOT_FOUND", ex.getMessage()));
    }

    @ExceptionHandler(Exception.class)                 // catch-all fallback
    ResponseEntity<ApiError> generic(Exception ex) {
        log.error("Unhandled", ex);
        return ResponseEntity.status(500).body(new ApiError("INTERNAL", "Unexpected error"));
    }
}

One advice class gives every endpoint the same error envelope, strips try/catch noise out of controllers, and is the natural place to log unexpected failures.

The quick path: @ResponseStatus on the exception

For trivial "this exception means that status" mappings, annotate the exception class itself:

@ResponseStatus(HttpStatus.NOT_FOUND)
class OrderNotFoundException extends RuntimeException {
    OrderNotFoundException(Long id) { super("Order " + id + " not found"); }
}

Now throwing it anywhere yields a 404 — no handler needed. The catch is that you don't control the body shape, so it's best for the simplest cases.

What Boot does if you handle nothing

Anything that reaches the container hits Boot's BasicErrorController, which renders a JSON error body for API clients (or the Whitelabel HTML page for browsers):

{ "timestamp": "...", "status": 500, "error": "Internal Server Error", "path": "/orders/999" }

That's a safety net, not a destination. Override it with a global advice, and never enable server.error.include-stacktrace in production — it leaks internals.

Which handler wins?

When several handlers could match, Spring chooses the most specific exception type, and a controller-local handler beats a global one for the same type. A thrown IllegalStateException goes to a handler for it before one for RuntimeException. Order overlapping catch-alls across advice beans with @Order.

Reshaping Spring's built-in exceptions

Validation failures, unreadable bodies, missing params, and wrong methods throw Spring's own MVC exceptions. Extend ResponseEntityExceptionHandler to reshape them all consistently:

@RestControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler {
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders h,
            HttpStatusCode status, WebRequest req) {
        var errors = ex.getBindingResult().getFieldErrors().stream()
            .map(f -> f.getField() + ": " + f.getDefaultMessage()).toList();
        return ResponseEntity.badRequest().body(new ApiError("VALIDATION", errors));
    }
}

You inherit handlers for every framework exception and override only the ones whose body you care about, then add your own domain handlers on top.

ProblemDetail: the standard error envelope

Spring 6 / Boot 3 ship ProblemDetail, an implementation of RFC 7807. Instead of inventing your own JSON shape, return the standard one:

@ExceptionHandler(OrderNotFoundException.class)
ProblemDetail handle(OrderNotFoundException ex) {
    ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    pd.setTitle("Order not found");
    pd.setProperty("orderId", ex.getId());       // custom extension member
    return pd;                                    // application/problem+json
}

For new APIs this is the best default: standardized, minimal code, still extensible. You can even turn it on for Spring's built-in exceptions with spring.mvc.problemdetails.enabled=true.

Designing a consistent error model

Whatever envelope you choose, give clients a stable machine-readable code separate from the human message, the right status, and a list of field errors when relevant:

record ApiError(String code, String message, Instant timestamp, List<String> details) {}

Return all validation errors at once rather than failing on the first, and keep internal messages and stack traces out of the response.

Log by severity

Don't log a stack trace for every 404 — that buries the real problems. Log expected client errors at WARN (no trace) and unexpected server errors at ERROR (with trace), and attach a correlation id so a log line can be tied to the response a client actually saw.

Know the boundaries

@ControllerAdvice only catches exceptions thrown during MVC handler invocation. Errors in @Async methods need an AsyncUncaughtExceptionHandler; errors in filters happen before dispatch and must be handled there. And to catch unknown-URL 404s in your advice, enable spring.mvc.throw-exception-if-no-handler-found and handle NoHandlerFoundException.

Recap

Map exceptions to responses with @ExceptionHandler, centralize them in one @RestControllerAdvice, use @ResponseStatus for trivial cases and ProblemDetail as your standard envelope, extend ResponseEntityExceptionHandler to reshape framework exceptions, and design a stable error model with machine-readable codes. Translate low-level exceptions into domain ones at the service boundary, log by severity, and remember the advice only covers MVC handler execution. Do that and your API's failures become as predictable as its successes.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel