What exactly does the reason attribute of @ResponseStatus (or the reason of ResponseStatusException) do to the response, and how does MessageSource / Spring Boot's error properties affect it?
answer
- reason → sendError(status, reason)
- sendError commits + forwards to /error → BasicErrorController builds body
- Boot include-message defaults to 'never' — reason hidden
- Spring 5.3: reason is a MessageSource code (i18n)
- @ExceptionHandler bypasses the sendError path
basics
~20 sWith a reason, the ResponseStatusExceptionResolver calls HttpServletResponse.sendError(status, reason) instead of sendError(status). sendError triggers the servlet error page (in Boot, /error and BasicErrorController), which builds the body. Since Spring 5.3 the reason can be resolved as a message code via MessageSource.
solid answer
~50 sThe reason changes how the resolver writes the response. Without a reason the ResponseStatusExceptionResolver calls response.sendError(statusCode); with a reason it calls response.sendError(statusCode, reason). sendError commits the response and hands control to the servlet container's error mechanism — in Spring Boot that means a forward to /error handled by BasicErrorController, which assembles the JSON/HTML body. Consequently the reason does not become the response body directly; it flows in as the container's error message and appears (or not) depending on the error attributes. Notably, Spring Boot suppresses the message by default: server.error.include-message is 'never', so the reason won't show unless set to 'always' or 'on_param'. Since Spring 5.3, the resolver can treat reason as a MessageSource code, so you can externalize/i18n it. Because sendError drives the flow, an @ExceptionHandler that writes its own body bypasses all this.
code
java · 10 lines// messages.properties: error.product.notFound=Product {0} was not found
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "error.product.notFound")
public class ProductNotFoundException extends RuntimeException { }
// application.properties
// server.error.include-message=always // otherwise 'message' is blank by default
// Programmatic equivalent; reason also runs through MessageSource since 5.3
// throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.product.notFound");go deeper
Know reason is an explanatory message attached to the status.
Know reason triggers sendError(status, reason) rather than sendError(status).
Explain the /error + BasicErrorController flow, include-message default, and MessageSource resolution.
Weigh reason/sendError vs central ProblemDetail advice for a consistent, safe error contract.
## What reason is `reason` is an optional attribute on `@ResponseStatus` and a constructor argument of `ResponseStatusException`. It is a human-readable explanation of the error. ## The mechanical difference: sendError Inside `ResponseStatusExceptionResolver.applyStatusAndReason(...)`: - **No reason** → `response.sendError(statusCode)`. - **With reason** → `response.sendError(statusCode, resolvedReason)`. `HttpServletResponse.sendError(...)` is a Servlet API call that (a) sets the status, (b) **commits** the response, and (c) tells the container to use its error-handling mechanism. It does NOT write your string as the raw body. In Spring Boot the container is configured to forward to the `/error` path, handled by `BasicErrorController`, which builds the actual body from the current `ErrorAttributes`. ## Where the reason ends up (and why it may vanish) Because the body is built by `BasicErrorController` from error attributes, the reason surfaces as the `message` attribute — subject to Boot's configuration: - `server.error.include-message` defaults to **`never`** (since Boot 2.3), so by default the `message` field is blank/omitted. Set it to `always` or `on_param` (`?message=...`) to expose it. - `server.error.include-exception`, `include-stacktrace`, `include-binding-errors` similarly gate other fields. This is a classic gotcha: developers set a nice `reason`, get a generic body, and think the reason was ignored — it was suppressed by `include-message=never`. ## MessageSource / i18n (Spring 5.3+) `ResponseStatusExceptionResolver` has `setMessageSource(MessageSource)` and is `MessageSourceAware`; in a Spring Boot app the context's `MessageSource` is injected. Since Spring 5.3 the `reason` is treated as a **message code**: if a matching entry exists in your `messages.properties`, the resolved (and possibly localized) text is used; otherwise the literal `reason` string is used as-is. So `@ResponseStatus(code = NOT_FOUND, reason = "error.product.notFound")` can resolve to a localized message. ## ResponseStatusException reason Same idea: `ResponseStatusException.getReason()` feeds the resolver, which likewise runs it through `sendError` and MessageSource. `ResponseStatusException` also has `getMessage()` combining status and reason for logs. ## Interaction with the resolver chain / @ExceptionHandler The reason/`sendError` path only applies when the `ResponseStatusExceptionResolver` handles the exception. If an `@ExceptionHandler` (via `ExceptionHandlerExceptionResolver`, which runs earlier) catches the exception and writes a `ResponseEntity`/`@ResponseBody`, that method controls the body entirely and the `sendError`/`/error` machinery is bypassed — your reason won't be used unless you read it yourself. ## Spring 6 ProblemDetail If the exception instead reaches `ResponseEntityExceptionHandler` (Spring 6), a `ProblemDetail` (RFC 7807) is produced and the reason maps to the problem `detail`, not the servlet error message. Which path applies depends on whether such a handler is registered. ## Practical guidance - Don't rely on `reason` alone for API error bodies; either flip `include-message=always` (careful about leaking internals) or, better, use a central `@ControllerAdvice`/`@ExceptionHandler` returning a structured body (or `ProblemDetail`) so you fully control the payload. - Use `reason` as a message code when you want i18n with minimal wiring.
- A teammate set reason but the JSON response shows an empty message. Why?Spring Boot's server.error.include-message defaults to 'never', so BasicErrorController omits the message. Set it to 'always' (or 'on_param') — or return a custom body via @ControllerAdvice.
- Does the reason string become the raw HTTP response body?No. It is passed to HttpServletResponse.sendError(status, reason), which commits the response and delegates to the container's error handling (/error, BasicErrorController); that machinery builds the body, exposing the reason only as the 'message' attribute if configured.
- How do you internationalize the reason?Since Spring 5.3 the ResponseStatusExceptionResolver runs reason through the context MessageSource, so use a message code (e.g. error.product.notFound) with entries in messages.properties.
saying these in an interview costs you the question
- Believing the reason string is written verbatim as the response body
- Not knowing include-message=never hides the reason by default
- Thinking reason works only as literal text and never via MessageSource