Spring REST Error Handling: Master Custom Messages & Exception Handling
Learn Spring REST error handling with custom messages and exception handling.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Basic knowledge of Spring Boot and REST APIs
- ✓Familiarity with Java exceptions and annotations
- 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.
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.
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.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.
Here's a typical ErrorResponse class:
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.
Here's an example:
spring.mvc.throw-exception-if-no-handler-found=true and add a handler.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.
Here's a simple custom exception:
What the Official Docs Won't Tell You
Here are the gotchas I've learned the hard way:
- @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.
- 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.
- 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.
- 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.
- 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.
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.
Here's an example test for a REST controller:
The Midnight Stack Trace Leak
- 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.
server.error.include-exception=falseserver.error.include-message=never| File | Command / Code | Purpose |
|---|---|---|
| DefaultErrorResponseExample.java | { | Why Default Error Handling Fails in Production |
| ErrorResponse.java | public class ErrorResponse { | Building a Custom Error Response Structure |
| GlobalExceptionHandler.java | @ControllerAdvice | Implementing @ControllerAdvice for Global Handling |
| ResourceNotFoundException.java | public class ResourceNotFoundException extends RuntimeException { | Creating Custom Exceptions for Your Domain |
| AsyncExceptionHandlerExample.java | @Configuration | What the Official Docs Won't Tell You |
| ErrorHandlingTest.java | @SpringBootTest | Testing Your Error Handling |
Key takeaways
Interview Questions on This Topic
How would you implement a global error handler in Spring Boot that returns a consistent JSON response?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
3 min read · try the examples if you haven't