Spring Boot
ResponseStatusException
exception handling
error messages
REST API

Exception message not included in response when throwing ResponseStatusException in Spring Boot

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When you throw a ResponseStatusException with a reason message in Spring Boot 2.3+, the message does not appear in the JSON error response by default. This is because Spring Boot changed the default behavior to hide error details for security reasons — exposing internal error messages to clients can leak implementation details. To include the message, you must set server.error.include-message=always in application.properties, or use a custom error handler.

The Problem

java
1@GetMapping("/users/{id}")
2public User getUser(@PathVariable Long id) {
3    return userRepository.findById(id)
4        .orElseThrow(() -> new ResponseStatusException(
5            HttpStatus.NOT_FOUND, "User not found with id: " + id
6        ));
7}

Expected response:

json
1{
2  "status": 404,
3  "error": "Not Found",
4  "message": "User not found with id: 42"
5}

Actual response (Spring Boot 2.3+):

json
1{
2  "status": 404,
3  "error": "Not Found",
4  "message": ""
5}

The message field is empty even though you passed "User not found with id: 42" to the exception.

Fix 1: Configure application.properties

properties
# application.properties
server.error.include-message=always

Or in YAML:

yaml
1# application.yml
2server:
3  error:
4    include-message: always

The options for include-message are:

  • never (default in Spring Boot 2.3+) — always empty
  • always — always included
  • on_param — included only when the request has a message parameter

After this change, the reason from ResponseStatusException appears in the response:

json
1{
2  "timestamp": "2025-03-02T10:15:30.000+00:00",
3  "status": 404,
4  "error": "Not Found",
5  "message": "User not found with id: 42",
6  "path": "/users/42"
7}

Fix 2: Custom Exception Handler with @ControllerAdvice

For full control over the error response format, use @ControllerAdvice:

java
1@RestControllerAdvice
2public class GlobalExceptionHandler {
3
4    @ExceptionHandler(ResponseStatusException.class)
5    public ResponseEntity<Map<String, Object>> handleResponseStatusException(
6            ResponseStatusException ex) {
7
8        Map<String, Object> body = new LinkedHashMap<>();
9        body.put("timestamp", LocalDateTime.now());
10        body.put("status", ex.getStatusCode().value());
11        body.put("error", ex.getStatusCode().toString());
12        body.put("message", ex.getReason());
13
14        return new ResponseEntity<>(body, ex.getStatusCode());
15    }
16}

This bypasses Spring Boot's default error handling entirely and lets you include or exclude any field.

Fix 3: Custom Error Attributes

Override DefaultErrorAttributes to customize what Spring Boot's default error response includes:

java
1@Component
2public class CustomErrorAttributes extends DefaultErrorAttributes {
3
4    @Override
5    public Map<String, Object> getErrorAttributes(
6            WebRequest webRequest, ErrorAttributeOptions options) {
7
8        // Include the message
9        options = options.including(ErrorAttributeOptions.Include.MESSAGE);
10
11        Map<String, Object> attrs = super.getErrorAttributes(webRequest, options);
12
13        // Optionally add custom fields
14        attrs.put("service", "user-service");
15
16        // Optionally remove fields
17        attrs.remove("trace");
18
19        return attrs;
20    }
21}

Fix 4: Custom Exception Classes

Instead of ResponseStatusException, define specific exception classes for cleaner error handling:

java
1@ResponseStatus(HttpStatus.NOT_FOUND)
2public class ResourceNotFoundException extends RuntimeException {
3    public ResourceNotFoundException(String message) {
4        super(message);
5    }
6}
java
1@RestControllerAdvice
2public class GlobalExceptionHandler {
3
4    @ExceptionHandler(ResourceNotFoundException.class)
5    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
6        ErrorResponse error = new ErrorResponse(
7            HttpStatus.NOT_FOUND.value(),
8            ex.getMessage(),
9            LocalDateTime.now()
10        );
11        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
12    }
13
14    @ExceptionHandler(IllegalArgumentException.class)
15    public ResponseEntity<ErrorResponse> handleBadRequest(IllegalArgumentException ex) {
16        ErrorResponse error = new ErrorResponse(
17            HttpStatus.BAD_REQUEST.value(),
18            ex.getMessage(),
19            LocalDateTime.now()
20        );
21        return ResponseEntity.badRequest().body(error);
22    }
23}
24
25record ErrorResponse(int status, String message, LocalDateTime timestamp) {}
java
1// Usage in controller
2@GetMapping("/users/{id}")
3public User getUser(@PathVariable Long id) {
4    return userRepository.findById(id)
5        .orElseThrow(() -> new ResourceNotFoundException(
6            "User not found with id: " + id
7        ));
8}

Other Hidden Error Properties

Spring Boot 2.3+ also hides stack traces and binding errors by default:

properties
1# Show everything (development only!)
2server.error.include-message=always
3server.error.include-stacktrace=on_param
4server.error.include-binding-errors=always
5server.error.include-exception=true

For production, keep the defaults (never) and use @ControllerAdvice to return only safe, user-facing messages.

Spring Boot Version History

VersionDefault include-messageBehavior
Spring Boot 2.0-2.2Messages included by defaultReason appears in response
Spring Boot 2.3+neverMessages hidden by default
Spring Boot 3.xneverSame as 2.3+, uses Jakarta namespace

The change was introduced in Spring Boot 2.3 (May 2020) as a security improvement to prevent accidental information leakage.

Common Pitfalls

  • Not knowing the default changed in Spring Boot 2.3: Code that worked in Spring Boot 2.2 silently stops showing error messages after upgrading. Add server.error.include-message=always if you relied on the old behavior.
  • Exposing internal details in production: Setting include-message=always in production can leak database column names, query details, or internal paths. Use @ControllerAdvice to sanitize messages for production and include-message=always only in development profiles.
  • Confusing reason with message: ResponseStatusException uses getReason(), not getMessage(). The getMessage() method returns the HTTP status description combined with the reason, while getReason() returns just your custom message.
  • Using @ResponseStatus on the exception class AND throwing ResponseStatusException: If you annotate a custom exception with @ResponseStatus and also wrap it in ResponseStatusException, the status codes may conflict. Use one approach consistently.
  • Forgetting to handle exceptions in reactive WebFlux: In WebFlux, ResponseStatusException works differently — it returns the message by default. The server.error.include-message property only applies to the servlet (MVC) stack.

Summary

  • Spring Boot 2.3+ hides ResponseStatusException reason messages by default for security
  • Set server.error.include-message=always in application.properties for the quickest fix
  • Use @RestControllerAdvice with @ExceptionHandler for full control over error responses
  • Define custom exception classes instead of ResponseStatusException for cleaner, more maintainable error handling
  • Keep error messages hidden in production and use structured error responses to avoid leaking internal details

Course illustration
Course illustration

All Rights Reserved.