Backend Development 10 min read

How Spring Boot Handles Errors: From /error to Custom Responses

This article explains how Spring Boot 2.4.11 processes exceptions, routes them to the default /error endpoint, and uses BasicErrorController along with ContentNegotiatingViewResolver and StaticView to generate HTML or JSON error responses based on the Accept header, including code examples and configuration details.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How Spring Boot Handles Errors: From /error to Custom Responses

Environment

Spring Boot 2.4.11

Configuration

The following demonstration uses the interface shown below.

<code>@RestController
@RequestMapping("/exceptions")
public class ExceptionsController {
    @GetMapping("/index")
    public Object index(int a) {
        if (a == 0) {
            throw new BusinessException();
        }
        return "exception";
    }
}
</code>

Default Error Display

By default, when an exception occurs Spring MVC forwards the request to /error . The response format depends on the Accept header, yielding either HTML or JSON.

HTML error response example
HTML error response example
JSON error response example
JSON error response example

Error Response Mechanism

Spring MVC’s DispatcherServlet catches exceptions and eventually forwards them to the BasicErrorController at /error .

<code>public class DispatcherServlet extends FrameworkServlet {
    protected void doDispatch(...) {
        try {
            // process result and check for exceptions
            processDispatchResult(...);
        }
    }
    private void processDispatchResult(...) {
        if (exception != null) {
            // resolve exception via HandlerExceptionResolver chain
            ModelAndView mv = processHandlerException(...);
            errorView = (mv != null);
        }
    }
    protected ModelAndView processHandlerException(...) {
        // default Rest API has no generic HandlerExceptionResolver
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            ModelAndView exMv = resolver.resolveException(...);
            if (exMv != null) {
                break;
            }
        }
        // if none resolves, rethrow the exception
        throw ex;
    }
}
</code>

The BasicErrorController defines two methods to handle different Accept header values.

<code>@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(
            getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }
}
</code>

When Accept: text/html ,

errorHtml

returns a ModelAndView that is later rendered by DispatcherServlet .

<code>if (mv != null && !mv.wasCleared()) {
    render(mv, request, response);
}

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    String viewName = mv.getViewName();
    View view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
    if (mv.getStatus() != null) {
        response.setStatus(mv.getStatus().value());
    }
    view.render(mv.getModelInternal(), request, response);
}
</code>

The ContentNegotiatingViewResolver selects the appropriate view based on the requested media types.

<code>public View resolveViewName(String viewName, Locale locale) throws Exception {
    List<MediaType> requestedMediaTypes = getMediaTypes(request);
    if (requestedMediaTypes != null) {
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }
    // ... fallback handling
}
</code>

The candidate views include ErrorMvcAutoConfiguration$StaticView and InternalResourceView . The resolver ultimately chooses StaticView for HTML responses.

View resolver chain diagram
View resolver chain diagram

When logging is set to

trace

, the selected view is logged, confirming that StaticView is used.

Trace log showing view selection
Trace log showing view selection

The StaticView renders a simple HTML page containing timestamp, error message, status, and optional stack trace.

<code>public class StaticView implements View {
    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setContentType(TEXT_HTML_UTF8.toString());
        StringBuilder builder = new StringBuilder();
        builder.append("<html><body><h1>Whitelabel Error Page</h1>")
               .append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
               .append("<div>" + model.get("timestamp") + "</div>")
               .append("<div>There was an unexpected error (type=" + htmlEscape(model.get("error")) + ", status=" + htmlEscape(model.get("status")) + ").</div>");
        if (model.get("message") != null) {
            builder.append("<div>" + htmlEscape(model.get("message")) + "</div>");
        }
        if (model.get("trace") != null) {
            builder.append("<div style='white-space:pre-wrap;'>" + htmlEscape(model.get("trace")) + "</div>");
        }
        builder.append("</body></html>");
        response.getWriter().append(builder.toString());
    }
}
</code>

Understanding this flow helps you customize error handling, replace the default whitelabel page, or provide richer JSON error bodies for API clients.

backendJavaSpring BootError HandlingException
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.