Exception Handling with @ExceptionHandler
Exception Handling with @ExceptionHandler
Every real API throws exceptions. A requested resource does not exist, a caller supplies a bad identifier, a downstream service is unreachable. The question is not whether exceptions will occur but where you want to deal with them, and what the caller receives when they do. Spring MVC's @ExceptionHandler mechanism gives you a clean, declarative answer: annotate a method with the exception type(s) you want to catch, and Spring dispatches to that method whenever one of those exceptions escapes a controller action.
The Problem with Try-Catch in Controller Methods
Before @ExceptionHandler existed, developers wrapped every action body in a try-catch, mapped the exception to a status code by hand, and returned an error response. That approach produces repetitive, hard-to-maintain code and scatter the error-handling logic everywhere instead of centralising it.
With @ExceptionHandler you define that mapping once, and every action in the controller automatically benefits.
Declaring a @ExceptionHandler Method
Place a method annotated with @ExceptionHandler inside a @RestController (or @Controller). Spring intercepts any exception of the declared type that bubbles out of any @RequestMapping method in the same class and routes it to your handler instead of propagating it.
Notice that getOrder no longer contains any try-catch. It simply delegates the happy-path work to the service and lets the exception propagate. Spring intercepts it before it reaches the servlet container and calls handleNotFound instead.
The Handler Method Signature
Handler methods are flexible. Spring resolves several useful parameters automatically:
- The exception itself — typed as the exception class (or a supertype).
HttpServletRequest/HttpServletResponse— raw servlet objects if you need them.WebRequest— a portable abstraction over the request.Locale,TimeZone— for localised error messages.Model— for MVC (non-REST) controllers that return a view name.
The return type is equally flexible: a ResponseEntity<T> (recommended for REST), a plain object (Spring uses the normal message converters), a ModelAndView (for MVC), or void if you write directly to the response.
Handling Multiple Exception Types in One Method
You can list several exception classes in the annotation value and receive the base type in the parameter:
RuntimeException.class, it will catch every unchecked exception not already claimed by a more specific handler in the same class. Spring always dispatches to the most specific matching handler first.
Accessing Request Information Inside the Handler
Sometimes you need to log the request path alongside the error, or include it in the error body so clients can correlate responses to their requests. Inject HttpServletRequest:
A Realistic ErrorResponse Record
Keep your error DTO consistent and serialisation-friendly. A Java record works perfectly — it is immutable, concise, and Jackson serialises it out of the box:
With this record an error response body looks like:
The Scope Limitation — and When to Move On
The key constraint of a controller-local @ExceptionHandler is its scope: it only catches exceptions thrown by methods in that same controller class. If you have twenty controllers and each one can throw OrderNotFoundException, you would need to copy the handler into every one of them.
@ExceptionHandler when the handling logic is genuinely specific to one controller — for example when error messages reference controller-level context or when you intentionally want different status codes for the same exception type in different parts of the API. For application-wide, cross-cutting error handling, move to @ControllerAdvice, which is the subject of the next lesson.
Putting It Together — A Complete Example
Both handler methods share the same ErrorResponse shape, map to appropriate HTTP status codes, and include the request path — giving API consumers everything they need to understand and act on the error without exposing internal stack traces.
Summary
@ExceptionHandler eliminates try-catch boilerplate from controller action methods by centralising error mapping in dedicated handler methods within the same controller. You declare which exception type(s) to intercept, choose the right HTTP status code, build a structured error body, and Spring does the dispatching. The trade-off is scope: handlers live in one class. In the next lesson you will see how @ControllerAdvice lifts that restriction and lets a single class handle exceptions across the entire application.