@ControllerAdvice and @ExceptionHandler

A Global Handler

@RestControllerAdvice (@ControllerAdvice + @ResponseBody) is a bean whose @ExceptionHandler methods apply to all controllers.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, Object> notFound(ResourceNotFoundException ex) {
        return Map.of("status", 404, "message", ex.getMessage());
    }

    @ExceptionHandler({InsufficientStockException.class, DuplicateSkuException.class})
    @ResponseStatus(HttpStatus.CONFLICT)
    public Map<String, Object> conflict(RuntimeException ex) {
        return Map.of("status", 409, "message", ex.getMessage());
    }
}

Example

curl -i localhost:8080/api/products/42

Example output

HTTP/1.1 404 Not Found
Content-Type: application/json

{"status":404,"message":"Product 42 not found"}

Which Handler Wins?

When several handlers could match, Spring picks the one for the closest exception type in the class hierarchy. A handler for Exception is therefore a safe "catch everything else" net. The plain-Java version of that lookup:

Example

import java.util.*;

public class Main {
    static final Map<Class<?>, Integer> handlers = new LinkedHashMap<>();

    static String handle(RuntimeException ex) {
        Class<?> type = ex.getClass();
        while (type != null) {                         // walk up: closest match first
            Integer status = handlers.get(type);
            if (status != null)
                return status + " (handler for " + type.getSimpleName() + "): " + ex.getMessage();
            type = type.getSuperclass();
        }
        return "500 (no handler)";
    }

    public static void main(String[] args) {
        handlers.put(ResourceNotFoundException.class, 404);
        handlers.put(BusinessException.class, 409);
        handlers.put(Exception.class, 500);

        System.out.println(handle(new ResourceNotFoundException("Product 42 not found")));
        System.out.println(handle(new InsufficientStockException("Only 2 left of Wireless Mouse")));
        System.out.println(handle(new BusinessException("Coupon expired")));
        System.out.println(handle(new IllegalStateException("Payment gateway down")));
    }
}

class BusinessException extends RuntimeException { BusinessException(String m) { super(m); } }
class ResourceNotFoundException extends BusinessException { ResourceNotFoundException(String m) { super(m); } }
class InsufficientStockException extends BusinessException { InsufficientStockException(String m) { super(m); } }

Output

404 (handler for ResourceNotFoundException): Product 42 not found
409 (handler for BusinessException): Only 2 left of Wireless Mouse
409 (handler for BusinessException): Coupon expired
500 (handler for Exception): Payment gateway down

Local Handlers

An @ExceptionHandler method inside a controller only handles exceptions from that controller, and it wins over the global advice. Use it rarely; one global handler keeps the format consistent.

Tip: Limit an advice to some controllers with @RestControllerAdvice(basePackages = "com.shopease.product") or assignableTypes = ....