00:00

Spring Boot Exception Handling Annotations

Last updated:

Spring Boot gives you a small set of annotations for turning exceptions into clean HTTP responses. Each one has a clear job: one catches an exception, one makes handlers global, one sets the status code, and one decides how the response body is written.

This page is an annotation-by-annotation reference: what each annotation does, its attributes, and how they work together. For the overall strategy, including error formats, logging and testing, see the companion guide Spring Boot Exception Handling.

1. The Annotations at a Glance

AnnotationPlaced onPurpose
@ExceptionHandlerA methodHandles one or more exception types and builds the response
@ControllerAdviceA classMakes its handler methods apply to many controllers
@RestControllerAdviceA class@ControllerAdvice + @ResponseBody, for REST APIs
@ResponseStatusAn exception class or a methodSets the HTTP status code of the response
@ResponseBodyA method or a classWrites the return value as the response body, usually JSON
@OrderAn advice classDecides which advice class is checked first

All of them except @Order, which comes from Spring’s core module, live in Spring’s web packages. They work in both Spring MVC and Spring WebFlux applications.

2. The Example Application

The examples on this page use a small course-enrollment API. Students enroll in courses, and a few things can go wrong:

CourseNotFoundException.java
public class CourseNotFoundException extends RuntimeException {

    public CourseNotFoundException(String courseCode) {
        super("Course " + courseCode + " does not exist");
    }
}
CourseFullException.java
public class CourseFullException extends RuntimeException {

    public CourseFullException(String courseCode, int capacity) {
        super("Course " + courseCode + " is full (" + capacity + " seats)");
    }
}
EnrollmentController.java
@RestController
@RequestMapping("/api/enrollments")
public class EnrollmentController {

    private final EnrollmentService enrollmentService;

    public EnrollmentController(EnrollmentService enrollmentService) {
        this.enrollmentService = enrollmentService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Enrollment enroll(@RequestBody EnrollmentRequest request) {
        // may throw CourseNotFoundException or CourseFullException
        return enrollmentService.enroll(request.studentId(), request.courseCode());
    }
}

3. @ExceptionHandler

@ExceptionHandler marks a method that runs when a matching exception is thrown. Placed inside a controller, it handles exceptions from that controller only:

EnrollmentController.java
@RestController
@RequestMapping("/api/enrollments")
public class EnrollmentController {

    // ... endpoints as above ...

    @ExceptionHandler(CourseFullException.class)
    public ResponseEntity<String> handleCourseFull(CourseFullException ex) {
        return ResponseEntity.status(HttpStatus.CONFLICT).body(ex.getMessage());
    }
}

3.1 Handling several exception types

List several classes in the annotation. The parameter must be a type they all share:

EnrollmentController.java
@ExceptionHandler({CourseNotFoundException.class, StudentNotFoundException.class})
public ResponseEntity<String> handleNotFound(RuntimeException ex) {
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}

3.2 Letting Spring infer the type

If the annotation has no value, Spring uses the type of the exception parameter:

EnrollmentController.java
@ExceptionHandler // handles CourseFullException, taken from the parameter
public ResponseEntity<String> handleCourseFull(CourseFullException ex) {
    return ResponseEntity.status(HttpStatus.CONFLICT).body(ex.getMessage());
}
AttributeMeaning
valueThe exception classes to handle; optional when the parameter type is enough
producesMedia types the handler can produce, for choosing between handlers by Accept header (Spring Framework 6.2+)

4. What a Handler Method Can Receive and Return

Handler methods are flexible. Spring supplies any of these parameters automatically, in any order:

Parameter typeWhat you get
The exception (or a supertype)The exception that was thrown
HttpServletRequest / WebRequestDetails of the current request, such as the path
HttpServletResponseDirect access to the response, for example to add headers
LocaleThe client’s language, for translated messages
HttpMethodGET, POST and so on
PrincipalThe logged-in user, if any

And it can return any of these:

Return typeResult
ResponseEntity<T>Full control over status, headers and body
ProblemDetailA standard RFC 9457 error body; the status is taken from the object
Any object + @ResponseBodyThe object is written as JSON with status 200, unless @ResponseStatus sets another
String (without @ResponseBody)A view name, for HTML error pages
voidAn empty body; combine with @ResponseStatus or write to HttpServletResponse yourself

Here is a handler that uses several parameters at once:

EnrollmentController.java
@ExceptionHandler
public ResponseEntity<ProblemDetail> handleCourseFull(CourseFullException ex,
                                                      HttpServletRequest request,
                                                      Locale locale) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
    problem.setInstance(URI.create(request.getRequestURI()));
    problem.setProperty("locale", locale.toLanguageTag());

    return ResponseEntity.status(HttpStatus.CONFLICT)
            .header("X-Waitlist-Available", "true")
            .body(problem);
}

5. How Spring Chooses a Handler

When an exception is thrown, Spring looks for a handler in this order:

  1. The controller that threw it. A local @ExceptionHandler always wins over global ones.
  2. Advice classes, in their @Order, checking only those whose scope includes this controller.
  3. Built-in resolvers, which honour @ResponseStatus on the exception class and Spring’s own exception types.
  4. The /error endpoint, if nothing else handled it.

Within one class, the closest matching type wins. Given these two handlers, a CourseFullException goes to the first one, and any other RuntimeException goes to the second:

EnrollmentController.java
@ExceptionHandler(CourseFullException.class)   // more specific: chosen for CourseFullException
public ResponseEntity<String> handleCourseFull(CourseFullException ex) { ... }

@ExceptionHandler(RuntimeException.class)      // less specific: everything else
public ResponseEntity<String> handleOther(RuntimeException ex) { ... }

Spring also checks the exception’s causes. If a CourseFullException is wrapped inside another exception, the handler for CourseFullException can still match it.

6. @ControllerAdvice

@ControllerAdvice turns a class into a shared helper for controllers. Its @ExceptionHandler methods then apply to every controller, so you write each handler once:

EnrollmentExceptionHandler.java
@ControllerAdvice
public class EnrollmentExceptionHandler {

    @ExceptionHandler
    @ResponseBody
    public ResponseEntity<String> handleNotFound(CourseNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }

    @ExceptionHandler
    @ResponseBody
    public ResponseEntity<String> handleCourseFull(CourseFullException ex) {
        return ResponseEntity.status(HttpStatus.CONFLICT).body(ex.getMessage());
    }
}

Use its attributes to limit which controllers it applies to:

AttributeApplies the advice toExample
basePackagesControllers in these packagesbasePackages = "com.javabytechie.school.api"
basePackageClassesControllers in the packages of these classes (safe when packages are renamed)basePackageClasses = EnrollmentController.class
assignableTypesThese controller classes and their subclassesassignableTypes = {EnrollmentController.class}
annotationsControllers carrying this annotationannotations = RestController.class

Advice classes can also hold @InitBinder and @ModelAttribute methods, which are shared across controllers in the same way.

7. @RestControllerAdvice

Writing @ResponseBody on every handler gets repetitive. @RestControllerAdvice combines @ControllerAdvice and @ResponseBody, so every handler writes its return value as the body:

EnrollmentExceptionHandler.java
@RestControllerAdvice
public class EnrollmentExceptionHandler {

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

    @ExceptionHandler
    public ProblemDetail handleCourseFull(CourseFullException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
        problem.setTitle("Course full");
        return problem;
    }
}

This is the usual choice for REST APIs. It relates to @ControllerAdvice exactly as @RestController relates to @Controller.

8. @ResponseStatus on Exception Classes

Put @ResponseStatus on an exception class to give it a fixed HTTP status. Whenever that exception escapes a controller and no @ExceptionHandler catches it, Spring uses that status:

EnrollmentClosedException.java
@ResponseStatus(HttpStatus.FORBIDDEN)
public class EnrollmentClosedException extends RuntimeException {

    public EnrollmentClosedException(String term) {
        super("Enrollment for " + term + " has closed");
    }
}

No handler is needed. A client that enrolls too late receives 403 from Spring Boot’s standard error response:

Response
{
  "timestamp": "2026-10-04T09:30:00.000+00:00",
  "status": 403,
  "error": "Forbidden",
  "path": "/api/enrollments"
}
AttributeMeaning
value / codeThe HTTP status; the two are aliases, so use either one
reasonA fixed reason text; Spring then sends the error with sendError

The exception message is hidden by default, because Spring Boot does not include messages in error responses unless server.error.include-message=always is set. Use this annotation for simple cases. When you need a custom body, write an @ExceptionHandler instead.

9. @ResponseStatus on Handler Methods

On a handler method, @ResponseStatus sets the status of the response the method returns. It is useful when the handler returns a plain object rather than a ResponseEntity:

EnrollmentExceptionHandler.java
public record ErrorMessage(String code, String message) {
}

@RestControllerAdvice
public class EnrollmentExceptionHandler {

    @ExceptionHandler
    @ResponseStatus(HttpStatus.CONFLICT)
    public ErrorMessage handleCourseFull(CourseFullException ex) {
        return new ErrorMessage("COURSE_FULL", ex.getMessage());
    }
}

The same annotation also works on normal controller methods, such as @ResponseStatus(HttpStatus.CREATED) on the enroll() method in section 2.

10. @ResponseBody

@ResponseBody tells Spring to convert a method’s return value into the response body, using JSON by default, instead of treating it as the name of an HTML template.

Where it comes fromEffect
@ResponseBody on a handler methodThat method’s result becomes the body
@ResponseBody on the advice classEvery handler in the class writes a body
@RestControllerAdvice / @RestControllerAlready include @ResponseBody
Returning ResponseEntity or ProblemDetailAlways written as the body, even without @ResponseBody

In an application that mixes HTML pages and a REST API, a plain @ControllerAdvice can serve both: handlers without @ResponseBody return error page names, and handlers with it return JSON.

MixedExceptionHandler.java
@ControllerAdvice
public class MixedExceptionHandler {

    // For the HTML website: shows templates/error/course-not-found.html
    @ExceptionHandler
    public String handlePageNotFound(CourseNotFoundException ex, Model model) {
        model.addAttribute("message", ex.getMessage());
        return "error/course-not-found";
    }

    // For the REST API
    @ExceptionHandler
    @ResponseBody
    public ResponseEntity<ErrorMessage> handleCourseFull(CourseFullException ex) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(new ErrorMessage("COURSE_FULL", ex.getMessage()));
    }
}

11. @Order for Multiple Advice Classes

With more than one advice class, add @Order so Spring checks them in a predictable order. A lower number means a higher priority:

EnrollmentExceptionHandler.java
@RestControllerAdvice(basePackageClasses = EnrollmentController.class)
@Order(1)
public class EnrollmentExceptionHandler {
    // specific handlers for the enrollment API
}

@RestControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
public class FallbackExceptionHandler {

    @ExceptionHandler
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorMessage handleAnything(Exception ex) {
        return new ErrorMessage("INTERNAL_ERROR", "An unexpected error occurred.");
    }
}

Spring uses the first advice, in order, that has a handler for the exception. Keep the broadest handlers in the class with the lowest priority.

12. Putting It All Together

This complete example combines every annotation on the page:

SchoolApiExceptionHandler.java
package com.javabytechie.school.api.error;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice(annotations = RestController.class)   // REST controllers only
@Order(Ordered.HIGHEST_PRECEDENCE)                           // checked before other advice
public class SchoolApiExceptionHandler {

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

    @ExceptionHandler
    public ProblemDetail handleCourseFull(CourseFullException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
        problem.setTitle("Course full");
        problem.setProperty("code", "COURSE_FULL");
        return problem;
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public ErrorMessage handlePrerequisite(PrerequisiteMissingException ex) {
        return new ErrorMessage("PREREQUISITE_MISSING", ex.getMessage());
    }
}

Meanwhile, EnrollmentClosedException still needs no handler at all, because its class carries @ResponseStatus(HttpStatus.FORBIDDEN).

13. Which Annotation Should You Use?

You want to...Use
Give an exception a fixed status, with no custom body@ResponseStatus on the exception class
Handle an exception that only one controller throws@ExceptionHandler inside that controller
Handle exceptions the same way across a REST API@RestControllerAdvice with @ExceptionHandler methods
Mix HTML error pages and JSON errors@ControllerAdvice, adding @ResponseBody to the JSON handlers
Return a plain object with a non-200 status@ResponseStatus on the handler method
Control which of several advice classes runs first@Order

14. Common Mistakes to Avoid

MistakeWhat goes wrongFix
Using @ControllerAdvice without @ResponseBody in a REST APISpring treats the returned object or string as a view nameUse @RestControllerAdvice
Handler parameter type does not fit all listed exceptionsThe handler itself fails with an error instead of producing a responseUse a common supertype as the parameter
Mapping NullPointerException to 400A server bug looks like the client’s faultLet unexpected errors become 500
Expecting @ResponseStatus(reason = ...) to return your own JSONSpring sends a standard error response insteadWrite an @ExceptionHandler for a custom body
Two advice classes without @OrderWhich handler runs may change between buildsAdd @Order
Expecting a handler in one controller to catch another controller’s exceptionsIt never runsMove it to an advice class

15. Summary

  • @ExceptionHandler turns a matching exception into a response; inside a controller it is local, inside an advice class it is shared.
  • @ControllerAdvice shares handlers across controllers, and its attributes limit where it applies.
  • @RestControllerAdvice is the same, plus @ResponseBody; it is the usual choice for REST APIs.
  • @ResponseStatus sets a status on an exception class or a method.
  • @ResponseBody writes the return value as the body instead of choosing a view.
  • @Order makes the order of several advice classes predictable.

Used together, they keep controllers free of try/catch blocks and give clients clear, consistent error responses. Continue with the Spring Boot Exception Handling guide for error formats, logging and testing.