Custom Error Pages in Spring Boot: Mastering Whitelabel ErrorController
Learn how to replace the default Whitelabel error page in Spring Boot with custom error pages using ErrorController, HTTP status handling, and production-ready templates..
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- ✓Java 17+ installed
- ✓Spring Boot 3.2+ project with spring-boot-starter-web
- ✓Basic understanding of Spring MVC and exception handling
• Spring Boot's default Whitelabel error page is a generic fallback that should be replaced for production. • You can customize error pages by implementing ErrorController or using error templates in resources/templates/error/. • For REST APIs, return JSON error responses instead of HTML. • Always include meaningful HTTP status codes, error messages, and correlation IDs. • Test error pages with real client scenarios, not just browser navigation.
Imagine your application is a restaurant. The Whitelabel error page is like a generic 'We're closed' sign when something goes wrong. Custom error pages are like a polite waiter who explains the specific problem (e.g., 'We're out of steak, but here's a menu with alternatives') and points you to the manager (support) if needed.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Every Spring Boot developer has seen it: the stark white page with black text saying 'Whitelabel Error Page' followed by a generic status code and a stack trace snippet. It's the default error view when something goes wrong — and it's a security nightmare for production. Not only does it expose internal implementation details (like class names and line numbers), but it also provides zero user guidance. In this article, we'll tear down that default and build a robust custom error handling system. We'll cover the ErrorController interface, template-based error pages for server-side rendered apps, and JSON error responses for REST APIs. I'll show you how to handle specific HTTP status codes (404, 500, 403) with tailored pages, log errors with correlation IDs, and avoid common pitfalls like infinite redirect loops. By the end, you'll have a production-ready error handling setup that's both user-friendly and developer-friendly. This is intermediate territory — you should already know Spring Boot basics, but we'll dive deep into the mechanics.
Understanding the Whitelabel Error Page
The Whitelabel error page is Spring Boot's fallback when no custom error handling is configured. It's generated by the BasicErrorController (registered automatically via @ConditionalOnMissingBean). The page shows the HTTP status code, the error message (if available), and a stack trace snippet. While convenient for development, it's unsuitable for production because it exposes internal details and provides no user guidance. The default behavior is controlled by server.error.whitelabel.enabled=true. When disabled, Spring Boot looks for a template in resources/templates/error/ matching the status code (e.g., 404.html, 5xx.html). The key interface is ErrorController, which has a single method getErrorPath() (deprecated in 2.3+) — now you implement ErrorController and map a custom endpoint. The default BasicErrorController returns a ModelAndView with attributes like status, error, message, timestamp, trace, path. You can override this by providing your own @Controller that implements ErrorController.
server.error.whitelabel.enabled=false to avoid accidental fallback. Then implement your own controller to ensure consistent behavior across all environments.What the Official Docs Won't Tell You
The official Spring Boot documentation covers the basics of custom error pages but glosses over critical production details. First, the BasicErrorController doesn't handle Accept headers well — it returns HTML for everything unless you override it. For REST APIs, you must check the request's Accept header or use @RequestMapping(produces = "application/json"). Second, error handling for async requests (e.g., @Async or CompletableFuture) is broken by default — exceptions thrown in async methods won't trigger the error controller unless you configure an AsyncExceptionHandler. Third, the trace attribute in the error model can include sensitive data like SQL queries or file paths; always strip it in production. Fourth, if you use Spring Security, the error controller might not be accessible for unauthenticated users — you need to permit access to /error in your security config. Finally, the order of error handling matters: @ExceptionHandler in controllers takes precedence over the error controller, which only catches unhandled exceptions. This can lead to inconsistent error formats if you mix both approaches.
Setting Up Custom Error Templates
To serve custom HTML error pages, create Thymeleaf (or FreeMarker) templates in src/main/resources/templates/error/. Spring Boot automatically resolves views based on the status code: 404.html for 404, 5xx.html for any 5xx, and error.html as a fallback. The template can access model attributes like status, error, message, path, and timestamp. For a more dynamic approach, use a single template that reads the status code and displays appropriate content. Use Thymeleaf's th:if to conditionally show different sections. For static resources (CSS, images), place them in src/main/resources/static/ and reference them with relative paths. Example: a 404 page with a friendly message and a link to the home page. For 5xx pages, avoid showing stack traces; instead, display a generic apology and a support contact. You can also use server.error.include-stacktrace=never in application.properties to globally disable stack traces in error responses.
Handling REST API Errors with JSON
For REST APIs, returning HTML error pages is useless. Instead, return a consistent JSON error response. The approach depends on whether you use @RestControllerAdvice or the ErrorController. The @RestControllerAdvice with @ExceptionHandler is ideal for known exceptions (like ResourceNotFoundException). For unhandled exceptions, the ErrorController catches them. To unify both, create a custom error response DTO and use it in both places. The DTO should include: status (HTTP status code), error (reason phrase), message (human-readable), path (request URI), timestamp, and traceId (for debugging). Use U for trace IDs. For validation errors (400 Bad Request), include a UID.randomUUID()errors field with field-level details. In Spring Boot 3.x, you can use ProblemDetail from RFC 7807, but it's less flexible. I prefer a custom DTO for full control. Remember to set the Content-Type header to application/problem+json if using ProblemDetail, or just application/json.
Advanced: Using ErrorAttributes for Fine-Grained Control
Spring Boot's DefaultErrorAttributes provides model attributes for error views. You can customize it by creating a bean that implements ErrorAttributes or extends DefaultErrorAttributes. Override getErrorAttributes() to add, remove, or modify attributes. For example, you might want to add a supportEmail attribute or remove the trace attribute. The ErrorAttributes interface has two methods: getErrorAttributes(WebRequest, ErrorAttributeOptions) and getError(WebRequest). The ErrorAttributeOptions lets you include or exclude stack trace, message, and binding errors. In production, always exclude stack trace. You can also add custom attributes like environment (from @Value("${spring.profiles.active}")) or appVersion. This is especially useful when you want to include business-specific error codes (e.g., ERR-001 for validation errors). The ErrorAttributes bean is automatically picked up by the BasicErrorController (or your custom controller).
Testing Custom Error Pages
Testing error pages is often neglected, leading to broken error handling in production. Use Spring Boot's MockMvc to simulate errors and verify the response. For HTML pages, check the status code and the presence of expected text (e.g., "Page Not Found"). For JSON responses, verify the structure and fields. Use @WebMvcTest with your error controller. To simulate a 404, use mockMvc.perform(get("/nonexistent")). For 500, you can throw an exception from a test endpoint. Also test the Accept header differentiation: send a request with Accept: application/json and verify JSON response. For integration tests, use @SpringBootTest with a random port and TestRestTemplate. Remember to test error pages for authenticated and unauthenticated users separately, as security filters might interfere. Use @WithMockUser for authenticated scenarios. Also test that static resources (CSS, images) are loaded correctly on error pages.
Common Pitfalls and How to Avoid Them
One common pitfall is infinite redirect loops. If your error controller redirects to another page that also triggers an error, you get a loop. Always return a view or response directly from the error controller, never redirect. Another pitfall is forgetting to permit access to /error in Spring Security. If your security config requires authentication for all endpoints, the error page won't be accessible for unauthenticated users, causing a 403 instead of the intended error page. Add requestMatchers("/error").permitAll() to your security config. A third pitfall is mixing @ExceptionHandler and ErrorController inconsistently. If you use @ExceptionHandler for some exceptions and the error controller for others, ensure both return the same format (HTML or JSON). Otherwise, clients get different response structures depending on the error type. Finally, don't forget to disable the Whitelabel page explicitly: server.error.whitelabel.enabled=false. Otherwise, if your custom controller fails, Spring Boot falls back to the default.
Production-Ready Error Handling with Logging and Monitoring
In production, error pages are not just for users — they're also a source of monitoring data. Every time an error page is served, log the event with a structured format (e.g., JSON logs). Include the traceId, status code, path, and the exception class (if available). Use a logging framework like Logback with a JSON encoder. For monitoring, expose error metrics via Micrometer (part of Spring Boot Actuator). Create a custom counter for each status code family (4xx, 5xx). For example, Counter.builder("http.server.errors").tag("status", "5xx").register(meterRegistry). You can then alert on spikes in 5xx errors. Also, consider using Spring Boot Admin or a dedicated APM tool to track error rates. For critical errors, send notifications to a Slack channel or PagerDuty. The error controller is an ideal place to add this instrumentation because it's the last line of defense.
The Whitelabel Leak: How a Default Error Page Exposed Our Internal API
- Never rely on the default Whitelabel page in production — it's a security risk.
- Always differentiate between human-facing and API error responses.
- Use correlation IDs in error responses to trace issues without exposing internals.
grep -r 'whitelabel' src/main/resources/application.propertiescurl -I http://localhost:8080/nonexistent| File | Command / Code | Purpose |
|---|---|---|
| DefaultErrorController.java | @Controller | Understanding the Whitelabel Error Page |
| ErrorControllerWithAcceptHeader.java | @Controller | What the Official Docs Won't Tell You |
| 404.html | Setting Up Custom Error Templates | |
| ErrorResponseDto.java | public class ErrorResponseDto { | Handling REST API Errors with JSON |
| CustomErrorAttributes.java | @Component | Advanced |
| ErrorControllerTest.java | @WebMvcTest(CustomErrorController.class) | Testing Custom Error Pages |
| SecurityConfig.java | @Configuration | Common Pitfalls and How to Avoid Them |
| MonitoredErrorController.java | @Controller | Production-Ready Error Handling with Logging and Monitoring |
Key takeaways
Interview Questions on This Topic
How does Spring Boot's default error handling work, and how would you replace it?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't