Exception Handling in Spring MVC
Learn how @ExceptionHandler and @ControllerAdvice centralize error handling across a Spring MVC application.
Introduction
Every application eventually deals with things going wrong - a requested record does not exist, a downstream service times out, or invalid input slips past validation. Spring MVC gives you a clean way to handle these situations without wrapping every controller method in try/catch: @ExceptionHandler methods, optionally centralized across the whole application with @ControllerAdvice. This lesson shows both.
- Why scattering try/catch across controllers does not scale.
- How @ExceptionHandler catches exceptions thrown from handler methods.
- How @ControllerAdvice centralizes exception handling application-wide.
- How to return structured, consistent error responses.
The Problem with Scattered try/catch
Without a dedicated mechanism, handling errors means wrapping the body of every controller method in try/catch, duplicating the same error-formatting logic dozens of times, and inevitably missing some cases. That duplication makes error responses inconsistent and the code harder to maintain.
// Repetitive, error-prone approach@GetMapping("/books/{id}")public ResponseEntity<Book> getBook(@PathVariable Long id) { try { return ResponseEntity.ok(bookService.findById(id)); } catch (BookNotFoundException ex) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); }}@ExceptionHandler in a Controller
A method annotated with @ExceptionHandler inside a controller catches exceptions of the specified type thrown by any handler method in that same controller, letting you remove the try/catch entirely.
@Controller@RequestMapping("/books")public class BookController {
@GetMapping("/{id}") public String bookDetails(@PathVariable Long id, Model model) { model.addAttribute("book", bookService.findById(id)); // throws if missing return "book-details"; }
@ExceptionHandler(BookNotFoundException.class) public String handleNotFound(BookNotFoundException ex, Model model) { model.addAttribute("message", ex.getMessage()); return "error-404"; }}bookService.findById(id) now simply throws BookNotFoundException when nothing is found, and handleNotFound() takes over automatically - no try/catch needed in bookDetails() itself.
Centralizing with @ControllerAdvice
An @ExceptionHandler method scoped to one controller only helps that controller. @ControllerAdvice lets you define exception handling once, in a dedicated class, and have it apply across every controller in the application.
@ControllerAdvicepublic class GlobalExceptionHandler {
@ExceptionHandler(BookNotFoundException.class) public String handleNotFound(BookNotFoundException ex, Model model) { model.addAttribute("message", ex.getMessage()); return "error-404"; }
@ExceptionHandler(Exception.class) public String handleGeneric(Exception ex, Model model) { model.addAttribute("message", "Something went wrong. Please try again."); return "error-generic"; }}Spring matches the most specific exception type first, so a handler for BookNotFoundException runs instead of the generic Exception handler when a BookNotFoundException is thrown, even though both are defined in the same class.
Returning Structured Error Responses
For a REST API, an @ExceptionHandler typically returns a structured error body plus the correct HTTP status, using ResponseEntity or @ResponseStatus, instead of a view name.
@RestControllerAdvicepublic class ApiExceptionHandler {
@ExceptionHandler(BookNotFoundException.class) public ResponseEntity<Map<String, String>> handleNotFound(BookNotFoundException ex) { Map<String, String> body = Map.of("error", ex.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); }}Click Run to see what this code prints.
Common Mistakes
- Catching Exception too broadly at the top and swallowing errors that should surface real bugs during development.
- Duplicating the same @ExceptionHandler logic in multiple controllers instead of centralizing it with @ControllerAdvice.
- Returning HTTP 200 for error cases because the response body happens to render successfully.
- Forgetting @RestControllerAdvice (or @ResponseBody) when building a JSON API, resulting in the error handler trying to resolve a view name instead.
- Leaking internal exception details (stack traces, SQL text) directly into a user-facing error response.
Best Practices
- Centralize exception handling with @ControllerAdvice (or @RestControllerAdvice for APIs) rather than repeating it per controller.
- Create specific, meaningful exception classes (BookNotFoundException) instead of throwing generic RuntimeException everywhere.
- Always set the correct HTTP status code for the error being reported.
- Keep a broad fallback handler for Exception.class as a safety net, but make it generic and never leak internals.
- Log the full exception server-side even when the response to the client is simplified.
Frequently Asked Questions
@RestControllerAdvice is @ControllerAdvice combined with @ResponseBody on every handler method, so returned objects are written directly to the response body as JSON, matching @RestController.
Yes - different handler methods within the same class can return a view name (String), a ResponseEntity, or a plain object, depending on what each specific error case needs.
No - it only catches exceptions thrown during the execution of controller/handler code that DispatcherServlet invokes. Exceptions thrown earlier in the filter chain need separate handling.
Key Takeaways
- @ExceptionHandler methods catch specific exception types thrown by handler methods, removing scattered try/catch.
- @ControllerAdvice centralizes exception handling across every controller in the application.
- @RestControllerAdvice combines @ControllerAdvice with @ResponseBody for JSON APIs.
- Spring matches the most specific applicable exception handler first.
- Error responses should use the correct HTTP status and never leak internal details.
Summary
Centralized exception handling keeps controllers focused on the happy path while still producing consistent, well-formed error responses. Having covered the request-handling side of Spring MVC, the course now turns to the data layer - starting with why Spring JDBC exists and how it cuts down raw JDBC boilerplate.