Close

Spring MVC - Annotation based Exception handling using @ExceptionHandler

[Last Updated: Sep 13, 2026]

The annotation @ExceptionHandler is used on a controller's methods that are not themselves handler methods, i.e. methods that are not already annotated with @RequestMapping (or its variants like @GetMappin) .

@ExceptionHandler can be used to return custom error response content to the caller.

Definition of ExceptionHandler

Version: 7.0.6
 package org.springframework.web.bind.annotation;
 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Reflective(ExceptionHandlerReflectiveProcessor.class)
 public @interface ExceptionHandler {
     @AliasFor("exception")
     Class<? extends Throwable>[] value() default {}; 1
     @AliasFor("value")
     Class<? extends Throwable>[] exception() default {}; 2
     String[] produces() default {}; 3
 }
1Exceptions handled by the annotated method.
2Exceptions handled by the annotated method. (Since 6.2)
3Media Types that can be produced by the annotated method. (Since 6.2)

Understanding @ExceptionHandler

Following are the important things to know about this annotation:

  1. The value element
    The 'value' element (aliased by 'exception' since Spring Framework 6.2 — the two are interchangeable) is for specifying the exception type(s) handled by the method. If an exception is thrown, then the annotated method will only be invoked when it matches the 'value'/'exception' type. The match is done based on the most specialized type, for example if:
    • method A has specified: @ExceptionHandler(RuntimeException.class)
    • and method B has specified: @ExceptionHandler(Exception.class)
    • a handler method throws RuntimeException, then A will be invoked.
    • if method A doesn't exist, then the thrown RuntimeException will instead match method B, because Exception is the superclass of RuntimeException.

  2. The produces element
    The 'produces' element (also since Spring Framework 6.2) lets you restrict an exception handler method to requests that ask for a specific media type (via the Accept header), which is useful when multiple @ExceptionHandler methods can match the same exception type but should return different content types.

  3. The parameters
    Methods annotated with this annotation are allowed to have very flexible signatures. They may take an exception argument (either the general Exception type or a more specific one), and this argument also serves as a matching hint when the annotation's 'value'/'exception' element is empty. The exception argument can refer to a top-level exception being propagated or to a nested cause inside a wrapper exception — any level of the cause chain can be matched, not just the outermost exception. Handler methods can additionally take request/response objects, the current HttpSession, WebRequest/NativeWebRequest, the current Locale, raw InputStream/OutputStream (or Reader/Writer), and a Model parameter — though the Model passed to an exception handler is always empty, since it isn't pre-populated with regular model attributes.
    Check out the complete list of parameter and return types here.

  4. When value/exception is empty
    If @ExceptionHandler's 'value'/'exception' element is empty, it will default to the exception type(s) specified in the method's parameter list. There should be at most one exception type specified in the method parameter list.

  5. Return types
    We can use @ResponseBody to return the error response straight from the method, return a view name, or — as of Spring Framework 6 — return a ProblemDetail or ErrorResponse object to produce a standardized RFC 9457 "problem details" error response body.

  6. Using @ResponseStatus
    We can also optionally use @ResponseStatus along with @ExceptionHandler to specify the response status code.

  7. Scope
    An @ExceptionHandler method defined directly inside a @Controller class only handles exceptions thrown by that same controller. To share exception-handling logic across multiple controllers, define the @ExceptionHandler methods instead in a class annotated with @ControllerAdvice (or @RestControllerAdvice for REST APIs) — those methods apply globally, or to a targeted subset of controllers if the advice is scoped.

  8. Ambiguity
    Mapping exactly the same exception type with multiple @ExceptionHandler methods within the same scope will cause this exception
      java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for ........................

  9. Background processing
    In the background, ExceptionHandlerExceptionResolver (an implementation of HandlerExceptionResolver) is used for exception resolving and invoking the @ExceptionHandler methods for us. By default, its instance is configured with DispatcherServlet, so we don't have to do any related configuration to use it.

Example


Example Controller

In the following controller, three handler methods throw different exceptions, and three methods are defined with @ExceptionHandler. Before running the code, try to mentally match each thrown exception type to the corresponding @ExceptionHandler's value to work out which method will be invoked in each scenario.

package com.logicbig.example;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import java.time.Year;

@Controller
public class ExampleController {

    @RequestMapping("/data/{year}")
    @ResponseBody
    public String handleRequest(@PathVariable("year") int year) throws Exception {
        if (Year.of(year).isBefore(Year.of(1990))) {
            throw new Exception("Year is before 1990: " + year);
        }
        return "data response " + year;
    }

    @RequestMapping("/test/{id}")
    @ResponseBody
    public String handleRequest2(@PathVariable("id") String id) {
        int i = Integer.parseInt(id);
        return "test response " + i;
    }

    @RequestMapping("/admin")
    @ResponseBody
    public String handleRequest3(HttpServletRequest request)
            throws UserNotLoggedInException {

        Object user = request.getSession()
                             .getAttribute("user");
        if (user == null) {
            throw new UserNotLoggedInException("user: " + user);
        }
        return "test response " + user;
    }

    @ExceptionHandler(NumberFormatException.class)
    public ModelAndView exceptionHandler(NumberFormatException re) {
        ModelAndView mav = new ModelAndView("errorPage");
        mav.addObject("exception", re);
        return mav;
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public String exceptionHandler2(Exception re) {
        return "Exception: " + re.getMessage();
    }

    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(UserNotLoggedInException.class)
    public ModelAndView exceptionHandler3(UserNotLoggedInException e) {
        ModelAndView mav = new ModelAndView("errorPage");
        mav.addObject("exception", e);
        return mav;
    }
}
package com.logicbig.example;

public class UserNotLoggedInException extends Exception {

    public UserNotLoggedInException (String message) {
        super(message);
    }
}

src/main/webapp/WEB-INF/views/errorPage.jsp

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>

<html>
<body>
<h3>This is custom exception page</h3>
<p>Exception Type: <b>${exception['class'].simpleName}</b></p>
 <p>Exception Message: <b>${exception.message}</b></p>

</body>
</html>

Running the example


To try examples, run embedded Jetty (configured in pom.xml of example project below):

mvn jetty:run

Output

We are going to show related code snippets to figure out the result easily.

/data/{year}

@Controller
public class ExampleController {

    @RequestMapping("/data/{year}")
    @ResponseBody
    public String handleRequest(@PathVariable("year") int year) throws Exception {
        if (Year.of(year).isBefore(Year.of(1990))) {
            throw new Exception("Year is before 1990: " + year);
        }
        return "data response " + year;
    }
    .............
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public String exceptionHandler2(Exception re) {
        return "Exception: " + re.getMessage();
    }
    .............
}
$ curl -si "http://localhost:8080/exception-handler-annotation/data/2007" | findstr /V /R "^Date: ^Content- ^Server:"
HTTP/1.1 200 OK

data response 2007
$ curl -si "http://localhost:8080/exception-handler-annotation/data/1985" | findstr /V /R "^Date: ^Content- ^Server:"
HTTP/1.1 400 Bad Request

Exception: Year is before 1990: 1985

/test/{id}

@Controller
public class ExampleController {
    .............
    @RequestMapping("/test/{id}")
    @ResponseBody
    public String handleRequest2(@PathVariable("id") String id) {
        int i = Integer.parseInt(id);
        return "test response " + i;
    }
    .............
    @ExceptionHandler(NumberFormatException.class)
    public ModelAndView exceptionHandler(NumberFormatException re) {
        ModelAndView mav = new ModelAndView("errorPage");
        mav.addObject("exception", re);
        return mav;
    }
    .............
}
$ curl -si "http://localhost:8080/exception-handler-annotation/test/2" | findstr /V /R "^Date: ^Content- ^Server:"
HTTP/1.1 200 OK

test response 2
$ curl -si "http://localhost:8080/exception-handler-annotation/test/abc" | findstr /V /R "^Date: ^Content- ^Server: ^Set-Cookie: ^Expires:"
HTTP/1.1 200 OK



<html>
<body>
<h3>This is custom exception page</h3>
<p>Exception Type: <b>NumberFormatException</b></p>
<p>Exception Message: <b>For input string: "abc"</b></p>

</body>
</html>

/admin

@Controller
public class ExampleController {
    .............
    @RequestMapping("/admin")
    @ResponseBody
    public String handleRequest3(HttpServletRequest request)
            throws UserNotLoggedInException {

        Object user = request.getSession()
                             .getAttribute("user");
        if (user == null) {
            throw new UserNotLoggedInException("user: " + user);
        }
        return "test response " + user;
    }
    .............
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(UserNotLoggedInException.class)
    public ModelAndView exceptionHandler3(UserNotLoggedInException e) {
        ModelAndView mav = new ModelAndView("errorPage");
        mav.addObject("exception", e);
        return mav;
    }
}
$ curl -si "http://localhost:8080/exception-handler-annotation/admin" | findstr /V /R "^Date: ^Content- ^Server: ^Set-Cookie: ^Expires:"
HTTP/1.1 403 Forbidden



<html>
<body>
<h3>This is custom exception page</h3>
<p>Exception Type: <b>UserNotLoggedInException</b></p>
<p>Exception Message: <b>user: null</b></p>

</body>
</html>

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 7.0.6 (Spring Web MVC)
     Version Compatibility: 3.2.9.RELEASE - 7.0.6Version List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 3.2.9.RELEASE
    • 3.2.10.RELEASE
    • 3.2.11.RELEASE
    • 3.2.12.RELEASE
    • 3.2.13.RELEASE
    • 3.2.14.RELEASE
    • 3.2.15.RELEASE
    • 3.2.16.RELEASE
    • 3.2.17.RELEASE
    • 3.2.18.RELEASE
    • 4.0.0.RELEASE
    • 4.0.1.RELEASE
    • 4.0.2.RELEASE
    • 4.0.3.RELEASE
    • 4.0.4.RELEASE
    • 4.0.5.RELEASE
    • 4.0.6.RELEASE
    • 4.0.7.RELEASE
    • 4.0.8.RELEASE
    • 4.0.9.RELEASE
    • 4.1.0.RELEASE
    • 4.1.1.RELEASE
    • 4.1.2.RELEASE
    • 4.1.3.RELEASE
    • 4.1.4.RELEASE
    • 4.1.5.RELEASE
    • 4.1.6.RELEASE
    • 4.1.7.RELEASE
    • 4.1.8.RELEASE
    • 4.1.9.RELEASE
    • 4.2.0.RELEASE
    • 4.2.1.RELEASE
    • 4.2.2.RELEASE
    • 4.2.3.RELEASE
    • 4.2.4.RELEASE
    • 4.2.5.RELEASE
    • 4.2.6.RELEASE
    • 4.2.7.RELEASE
    • 4.2.8.RELEASE
    • 4.2.9.RELEASE
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE
    • 5.3.0
    • 5.3.1
    • 5.3.2
    • 5.3.3
    • 5.3.4
    • javax.servlet-api:4.x
    • 5.3.5
    • 5.3.6
    • 5.3.7
    • 5.3.8
    • 5.3.9
    • 5.3.10
    • 5.3.11
    • 5.3.12
    • 5.3.13
    • 5.3.14
    • 5.3.15
    • 5.3.16
    • 5.3.17
    • 5.3.18
    • 5.3.19
    • 5.3.20
    • 5.3.21
    • 5.3.22
    • 5.3.23
    • 5.3.24
    • 5.3.25
    • 5.3.26
    • 5.3.27
    • 5.3.28
    • 5.3.29
    • 5.3.30
    • 5.3.31
    • 5.3.32
    • 5.3.33
    • 5.3.34
    • 5.3.35
    • 5.3.36
    • 5.3.37
    • 5.3.38
    • 5.3.39
    • javax.* -> jakarta.*
      jakarta.servlet-api:6.x
      Java 17 min
    • 6.0.0
    • 6.0.1
    • 6.0.2
    • 6.0.3
    • 6.0.4
    • 6.0.5
    • 6.0.6
    • 6.0.7
    • 6.0.8
    • 6.0.9
    • 6.0.10
    • 6.0.11
    • 6.0.12
    • 6.0.13
    • 6.0.14
    • 6.0.15
    • 6.0.16
    • 6.0.17
    • 6.0.18
    • 6.0.19
    • 6.0.20
    • 6.0.21
    • 6.0.22
    • 6.0.23
    • 6.1.0
    • 6.1.1
    • 6.1.2
    • 6.1.3
    • 6.1.4
    • 6.1.5
    • 6.1.6
    • 6.1.7
    • 6.1.8
    • 6.1.9
    • 6.1.10
    • 6.1.11
    • 6.1.12
    • 6.1.13
    • 6.1.14
    • 6.1.15
    • 6.1.16
    • 6.1.17
    • 6.1.18
    • 6.1.19
    • 6.1.20
    • 6.1.21
    • 6.2.0
    • 6.2.1
    • 6.2.2
    • 6.2.3
    • 6.2.4
    • 6.2.5
    • 6.2.6
    • 6.2.7
    • 6.2.8
    • 6.2.9
    • 6.2.10
    • 6.2.11
    • 6.2.12
    • 6.2.13
    • 6.2.14
    • 6.2.15
    • 6.2.16
    • 6.2.17
    • 6.2.18
    • 6.2.19
    • 7.0.0
    • 7.0.1
    • 7.0.2
    • 7.0.3
    • 7.0.4
    • 7.0.5
    • 7.0.6

    Versions in green have been tested.

  • jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
  • JDK 25
  • Maven 3.9.11

Spring MVC - @ExceptionHandler Example Select All Download
  • exception-handler-annotation
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • ExampleController.java
          • webapp
            • WEB-INF
              • views

    See Also

    Join