Home Java Spring REST Error Handling: Master Custom Messages & Exception Handling
Intermediate 3 min · July 14, 2026

Spring REST Error Handling: Master Custom Messages & Exception Handling

Learn Spring REST error handling with custom messages and exception handling.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of Spring Boot and REST APIs
  • Familiarity with Java exceptions and annotations
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use @ControllerAdvice for global exception handling.
  • Extend ResponseEntityExceptionHandler for Spring MVC defaults.
  • Return consistent error structures with HTTP status codes.
  • Avoid leaking stack traces in production responses.
  • Use @ExceptionHandler for controller-specific handling.
✦ Definition~90s read
What is Spring REST Error Handling?

Spring REST error handling is a mechanism to catch exceptions in your API and return structured, consistent JSON error responses to clients.

Think of your API as a restaurant kitchen.
Plain-English First

Think of your API as a restaurant kitchen. When something goes wrong (burned steak, missing ingredient), you don't just yell "Error!" You tell the waiter exactly what happened so they can fix it. Spring's error handling is like that waiter—catching problems and sending clear messages back to the customer.

Every API needs to fail gracefully. I've seen too many production outages caused by poorly handled exceptions—500 errors with no context, stack traces leaking into responses, or generic messages that leave clients guessing. In this article, I'll show you how to build a robust error handling system for Spring REST APIs that your team will thank you for. We'll cover custom exception classes, global handlers with @ControllerAdvice, consistent error response structures, and the gotchas that official docs skip. By the end, you'll handle errors like a senior dev: predictable, informative, and secure.

Why Default Error Handling Fails in Production

Spring Boot's default error handling is a great starting point for prototypes, but it's a liability in production. By default, it returns a BasicErrorController response with fields like 'error', 'message', and 'trace'. The 'trace' field can include full stack traces—exposing internal package names, SQL queries, and even sensitive data. I've seen a startup's entire database schema leaked because a constraint violation exception bubbled up.

Moreover, the default response structure is not customizable per exception type. You'll get the same generic 500 for a NullPointerException as you would for a custom BusinessException. Clients can't programmatically handle different error scenarios.

What the docs don't tell you: the default ErrorMvcAutoConfiguration is triggered by the 'error' path, and it's hard to override completely. If you disable it via server.error.whitelabel.enabled=false, you still get an empty 500 response. You need to implement your own error handling strategy.

My advice: never go to production without a custom error handling layer. It's one of the first things I add to any Spring Boot project.

DefaultErrorResponseExample.javaJAVA
1
2
3
4
5
6
7
8
// Example of default response (from BasicErrorController)
{
    "timestamp": "2025-04-07T10:15:30.000+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "could not execute statement; SQL [insert into ...]",
    "trace": "org.hibernate.exception.ConstraintViolationException: could not execute statement\n at ..."
}
Output
{
"timestamp": "2025-04-07T10:15:30.000+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "could not execute statement; SQL [insert into ...]",
"trace": "org.hibernate.exception.ConstraintViolationException: could not execute statement\n at ..."
}
⚠ Security Risk: Stack Trace Leak
📊 Production Insight
I once debugged a case where a developer had set server.error.include-exception=true in application.properties, thinking it helped debugging. It exposed Hibernate SQL in the response. Took us hours to find the leak.
🎯 Key Takeaway
Default error handling is for development only. Always implement custom handling for production.

Building a Custom Error Response Structure

The first step is to define a consistent error response DTO. This DTO will be returned for every error. A common structure includes: timestamp, status, error (short code), message (user-friendly), and path. You can also include validation errors as a list.

ErrorResponse.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.time.LocalDateTime;
import java.util.List;

public class ErrorResponse {
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;
    private List<FieldError> fieldErrors;

    // Constructors, getters, setters
    public ErrorResponse(int status, String error, String message, String path) {
        this.timestamp = LocalDateTime.now();
        this.status = status;
        this.error = error;
        this.message = message;
        this.path = path;
    }

    // Inner class for field-level errors
    public static class FieldError {
        private String field;
        private String message;

        public FieldError(String field, String message) {
            this.field = field;
            this.message = message;
        }
        // getters
    }
}
📊 Production Insight
Include a unique error ID (UUID) in your response for correlating with server logs. It's a lifesaver when clients report issues.
🎯 Key Takeaway
Use a single ErrorResponse DTO across all handlers for consistency.

Implementing @ControllerAdvice for Global Handling

The @ControllerAdvice annotation allows you to define a global interceptor that applies to all controllers. Within it, you define @ExceptionHandler methods for specific exceptions.

Spring provides ResponseEntityExceptionHandler, which is a convenient base class that handles Spring MVC exceptions (like MethodArgumentNotValidException, NoHandlerFoundException, etc.). I recommend extending it and overriding its methods.

GlobalExceptionHandler.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
                .map(fe -> new ErrorResponse.FieldError(fe.getField(), fe.getDefaultMessage()))
                .collect(Collectors.toList());
        ErrorResponse body = new ErrorResponse(status.value(), "Validation Failed", "Input validation failed", request.getDescription(false));
        body.setFieldErrors(fieldErrors);
        return new ResponseEntity<>(body, status);
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) {
        ErrorResponse body = new ErrorResponse(HttpStatus.NOT_FOUND.value(), "Not Found", ex.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllUncaught(Exception ex, WebRequest request) {
        // Log full stack trace
        log.error("Unhandled exception", ex);
        ErrorResponse body = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal Server Error", "An unexpected error occurred", request.getDescription(false));
        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
💡Order Matters
📊 Production Insight
Don't forget to handle NoHandlerFoundException. By default, Spring MVC doesn't throw it for missing endpoints. Set spring.mvc.throw-exception-if-no-handler-found=true and add a handler.
🎯 Key Takeaway
Extend ResponseEntityExceptionHandler and override methods for Spring MVC exceptions. Add custom @ExceptionHandler for your domain exceptions.

Creating Custom Exceptions for Your Domain

Instead of throwing generic exceptions, create domain-specific ones. This makes error handling more precise and allows clients to react differently. For example, in a payment system, you might have InsufficientFundsException, PaymentDeclinedException, etc.

Each custom exception can carry additional data (like the account ID or transaction reference) that you can include in the error response.

ResourceNotFoundException.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ResourceNotFoundException extends RuntimeException {
    private final String resourceName;
    private final String fieldName;
    private final Object fieldValue;

    public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
        super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
        this.resourceName = resourceName;
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }

    // getters
}
📊 Production Insight
Avoid making custom exceptions checked exceptions. Unchecked exceptions (extending RuntimeException) are simpler and don't clutter your service layer with throws declarations.
🎯 Key Takeaway
Use custom exceptions to carry domain-specific context for richer error responses.

What the Official Docs Won't Tell You

  1. @ControllerAdvice order: If you have multiple @ControllerAdvice beans, they are ordered by @Order annotation. Without it, the order is undefined. I've seen cases where a generic handler with @Order(Ordered.LOWEST_PRECEDENCE) overrode a specific one because of import order. Always explicitly set @Order.
  2. ResponseEntityExceptionHandler limitations: It only handles exceptions thrown by Spring MVC itself (like MethodArgumentNotValidException). For exceptions thrown in your code (e.g., ResourceNotFoundException), you need separate @ExceptionHandler methods.
  3. Async exception handling: Exceptions thrown in @Async methods or reactive streams are not caught by @ControllerAdvice unless you handle them explicitly. For @Async, you need a custom AsyncUncaughtExceptionHandler. For WebFlux, use @ExceptionHandler in the controller or a global error handler.
  4. Security exceptions: Spring Security exceptions (like AccessDeniedException) are not caught by @ControllerAdvice by default. They are handled by the ExceptionTranslationFilter. You can customize by implementing AccessDeniedHandler or AuthenticationEntryPoint.
  5. Validation groups: If you use validation groups (@Validated with groups), the MethodArgumentNotValidException still uses the default group. You'll need to manually inspect the ConstraintViolationException in your handler to get group-specific errors.
AsyncExceptionHandlerExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> {
            // Log or handle async exception
            log.error("Async method {} threw exception", method.getName(), ex);
        };
    }
}
🔥Security Exception Handling
📊 Production Insight
I once spent hours debugging why a @ControllerAdvice wasn't catching a ConstraintViolationException from Hibernate Validator. Turns out, validation errors are wrapped in MethodArgumentNotValidException only if they occur in @Valid parameters. For programmatic validation, you need to handle ConstraintViolationException directly.
🎯 Key Takeaway
Official docs don't cover ordering, async, security, or validation groups. Be aware of these edge cases.

Testing Your Error Handling

You must test error scenarios just like happy paths. Use Spring MockMvc to simulate requests that trigger exceptions and verify the response structure.

ErrorHandlingTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
@AutoConfigureMockMvc
class PaymentControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void whenPaymentFails_thenReturnsErrorResponse() throws Exception {
        mockMvc.perform(post("/api/payments")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"amount\": -10}"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error").value("Validation Failed"))
                .andExpect(jsonPath("$.fieldErrors[0].field").value("amount"));
    }
}
📊 Production Insight
Include tests for security errors (401, 403) and 404 errors. These are often overlooked and can break when you change security config or endpoint paths.
🎯 Key Takeaway
Write integration tests for error scenarios to ensure your handlers work as expected.
● Production incidentPOST-MORTEMseverity: high

The Midnight Stack Trace Leak

Symptom
Users saw raw SQL and table names in error responses when a foreign key constraint failed.
Assumption
Developer assumed Spring Boot's default error handling was safe for production.
Root cause
No custom @ControllerAdvice; default ErrorMvcAutoConfiguration exposed exception details.
Fix
Implemented @ControllerAdvice with ResponseEntityExceptionHandler and a sanitized error response DTO.
Key lesson
  • Never rely on default error handling in production.
  • Always sanitize exception messages before returning to client.
  • Use a consistent error response structure (e.g., timestamp, status, message, path).
  • Log full stack traces server-side; return minimal info client-side.
  • Test error scenarios in staging with real client expectations.
Production debug guideSymptom to Action5 entries
Symptom · 01
Client receives 500 but no details in logs
Fix
Check if exception is swallowed by a generic catch block. Enable debug logging for your handlers.
Symptom · 02
Error response contains stack trace
Fix
Immediately add @ControllerAdvice and override handleException to return sanitized response. Set server.error.include-exception=false.
Symptom · 03
Validation errors return 400 with no field details
Fix
Override handleMethodArgumentNotValid in your handler to extract field errors and include them in response.
Symptom · 04
Custom exception not caught by handler
Fix
Verify the exception class is in the handler's @ExceptionHandler signature and that the handler is registered (e.g., component scan).
Symptom · 05
404 errors return default Whitelabel Error Page
Fix
Disable Whitelabel via server.error.whitelabel.enabled=false and implement a custom ErrorController or use @ControllerAdvice for NoHandlerFoundException.
★ Quick Debug Cheat SheetFast actions for common error handling issues.
Stack trace in response
Immediate action
Set server.error.include-exception=false and implement @ControllerAdvice.
Commands
server.error.include-exception=false
server.error.include-message=never
Fix now
Add @ControllerAdvice class that returns sanitized ErrorResponse.
Validation errors not detailed+
Immediate action
Override handleMethodArgumentNotValid in your handler.
Commands
Override handleMethodArgumentNotValid
Extract field errors from BindingResult
Fix now
Return map of field -> error message in response.
Custom exception not handled+
Immediate action
Verify @ExceptionHandler signature and component scan.
Commands
Check @ExceptionHandler(YourException.class)
Ensure handler class is @ControllerAdvice and scanned
Fix now
Add @ExceptionHandler method for your exception.
404 returns HTML page+
Immediate action
Disable Whitelabel and add custom handler.
Commands
server.error.whitelabel.enabled=false
spring.mvc.throw-exception-if-no-handler-found=true
Fix now
Implement ErrorController or handle NoHandlerFoundException.
Generic 500 for all errors+
Immediate action
Check for unhandled exceptions and add specific handlers.
Commands
Check logs for unhandled exception type
Add @ExceptionHandler for that type
Fix now
Implement fallback handler with generic message and log full details.
StrategyScopeCustomizationUse Case
@ExceptionHandler in controllerSingle controllerPer controllerSimple apps with few controllers
@ControllerAdviceGlobalCentralizedMost applications
HandlerExceptionResolverLow-levelFlexibleAdvanced custom handling
ErrorControllerGlobal (low-level)Full controlWhen you need to override default error path
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
DefaultErrorResponseExample.java{Why Default Error Handling Fails in Production
ErrorResponse.javapublic class ErrorResponse {Building a Custom Error Response Structure
GlobalExceptionHandler.java@ControllerAdviceImplementing @ControllerAdvice for Global Handling
ResourceNotFoundException.javapublic class ResourceNotFoundException extends RuntimeException {Creating Custom Exceptions for Your Domain
AsyncExceptionHandlerExample.java@ConfigurationWhat the Official Docs Won't Tell You
ErrorHandlingTest.java@SpringBootTestTesting Your Error Handling

Key takeaways

1
Always implement a custom @ControllerAdvice for production error handling.
2
Use a consistent ErrorResponse DTO with fields like timestamp, status, error, message, path.
3
Extend ResponseEntityExceptionHandler to handle Spring MVC exceptions easily.
4
Create domain-specific custom exceptions for precise error handling.
5
Test error scenarios thoroughly with integration tests.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How would you implement a global error handler in Spring Boot that retur...
Q02SENIOR
How do you handle exceptions thrown in asynchronous methods?
Q03JUNIOR
What is the purpose of ResponseEntityExceptionHandler?
Q01 of 03SENIOR

How would you implement a global error handler in Spring Boot that returns a consistent JSON response?

ANSWER
Create a class annotated with @ControllerAdvice extending ResponseEntityExceptionHandler. Override methods like handleMethodArgumentNotValid for validation errors. Add @ExceptionHandler methods for custom exceptions. Define a standard ErrorResponse DTO with fields like timestamp, status, error, message, and path.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between @ExceptionHandler and @ControllerAdvice?
02
How do I return a custom error response for validation errors?
03
Why is my @ControllerAdvice not catching exceptions?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

3 min read · try the examples if you haven't

Previous
HTTP PUT vs POST in REST APIs: Idempotency and Resource Creation
94 / 121 · Spring Boot
Next
Spring @Controller and @RestController Annotations: Differences and Use Cases