Close

Spring MVC - Using @ResponseStatus on Exception classes

[Last Updated: Sep 18, 2026]

The exception classes that we create for our application can be annotated with @ResponseStatus. An unhandled exception that is an instance of such a class will carry the specified HTTP status code to the client.

If such an exception is wrapped inside another exception, the status code will still be sent in the response, because the underlying exception resolver looks recursively for @ResponseStatus on cause exceptions (since Spring 4.2).

This feature is implemented by ResponseStatusExceptionResolver (another implementation of HandlerExceptionResolver). By default, an instance of this resolver is auto-registered with the DispatcherServlet, so no additional configuration is required to use it.

Note: Since Spring 5.0, an alternative to defining a dedicated exception class is to throw a ResponseStatusException directly from the handler method, which lets you set the status (and an optional reason) programmatically without creating a new exception type. The approach shown below, using a custom exception class annotated with @ResponseStatus, remains fully supported and is still preferable when the exception type itself carries semantic meaning elsewhere in the application.


Example

Using @ResponseStatus on an exception class

package com.logicbig.example;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.FORBIDDEN)
public class UserNotLoggedInException extends Exception {

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

A controller handler throwing the exception

package com.logicbig.example;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import jakarta.servlet.http.HttpServletRequest;

@Controller
public class ExampleController {

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

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

    //nested exceptions
    @RequestMapping("/test")
    public void handleRequest2 () throws Exception {
        throw new Exception(new UserNotLoggedInException(null));
    }
}

The exception thrown should not be handled elsewhere in code or by other exception resolvers — for example, it shouldn't be handled by @ExceptionHandler, because doing so would override the status code specified on the exception class via @ResponseStatus.

Running the example

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

mvn jetty:run

Output

/admin


/test

In this case, we wrapped UserNotLoggedInException inside a java.lang.Exception instance (only works Spring starting MVC 4.2.0.RELEASE)


Without @ResponseStatus on the exception class:

If we remove @ResponseStatus from UserNotLoggedInException.java, then:

Normally, any unhandled exception thrown will return an HTTP 500 response (Internal Server Error).

Integration Test

package com.logicbig.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;

@SpringJUnitWebConfig(AppConfig.class)
public class ExampleControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvcTester mockMvcTester;

    @BeforeEach
    public void setup() {
        mockMvcTester = MockMvcTester.from(wac);
    }

    @Test
    public void admin_withoutSession_returnsForbidden() {
        assertThat(mockMvcTester.get().uri("/admin").exchange())
                .hasStatus(HttpStatus.FORBIDDEN);
    }

    @Test
    public void admin_withSession_returnsOkWithBody() {
        MockHttpSession session = new MockHttpSession();
        session.setAttribute("user", "joe");

        assertThat(mockMvcTester.get().uri("/admin").session(session)
                                .exchange())
                .hasStatusOk()
                .bodyText().isEqualTo("test response joe");
    }

    @Test
    public void test_wrappedException_returnsForbidden() {
        // On Spring 4.2.0, ResponseStatusExceptionResolver looks recursively at
        // cause exceptions, so the @ResponseStatus(FORBIDDEN) on the wrapped
        // UserNotLoggedInException is honored even though a plain Exception is thrown.
        assertThat(mockMvcTester.get().uri("/test").exchange())
                .hasStatus(HttpStatus.FORBIDDEN);
    }
}
mvn clean test -Dtest="ExampleControllerTest"

Output

$ mvn clean test -Dtest="ExampleControllerTest"
[INFO] Scanning for projects...
[INFO]
[INFO] --------< com.logicbig.example:exception-type-response-status >---------
[INFO] Building exception-type-response-status 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ exception-type-response-status ---
[INFO] Deleting D:\example-projects\spring-mvc\exception-type-response-status\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ exception-type-response-status ---
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\exception-type-response-status\src\main\resources
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ exception-type-response-status ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 4 source files with javac [debug target 25] to target\classes
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ exception-type-response-status ---
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\exception-type-response-status\src\test\resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ exception-type-response-status ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 2 source files with javac [debug target 25] to target\test-classes
[INFO]
[INFO] --- surefire:3.2.5:test (default-test) @ exception-type-response-status ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.logicbig.example.ExampleControllerTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.355 s -- in com.logicbig.example.ExampleControllerTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.338 s
[INFO] Finished at: 2026-09-18T07:44:44-05:00
[INFO] ------------------------------------------------------------------------
INFO: Completed initialization in 2 ms
INFO: Completed initialization in 1 ms
INFO: Completed initialization in 1 ms

Other uses of @ResponseStatus

  1. It can be used on the controller's handler methods, i.e. along with @RequestMapping (or a variants such as @GetMapping/@PostMapping). It can also be used at the @Controller class level, in which case it is inherited by all methods.
  2. It can be used on the exception handler methods, i.e. along with @ExceptionHandler.

Example Project

Dependencies and Technologies Used:

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

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 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.

  • spring-test 7.0.6 (Spring TestContext Framework)
  • jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
  • junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
  • hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
  • assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
  • JDK 25
  • Maven 3.9.11

Spring MVC - @ResponseStatus Example Select All Download
  • exception-type-response-status
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • ExampleController.java
        • test
          • java
            • com
              • logicbig
                • example

    See Also

    Join