LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 2020 min read

Global Exception Handling

Use @ControllerAdvice and @ExceptionHandler to catch exceptions from anywhere in your application and return consistent, well-shaped error responses.

Introduction

In the last lesson, you saw that Spring Boot's default validation error response works, but its shape isn't something you control. As your API grows, you'll want every error — validation failures, missing resources, unexpected exceptions — to come back in one consistent, predictable JSON shape, no matter which controller or method threw it. @ControllerAdvice combined with @ExceptionHandler gives you exactly that: a single, centralized place to catch and format exceptions from your entire application.

What You Will Learn
  • Why scattered try/catch blocks don't scale as an error-handling strategy.
  • How @ControllerAdvice and @ExceptionHandler work together.
  • How to define and throw a custom exception for domain-specific errors.
  • How to override Spring's default validation error format with your own.

The Problem with Scattered Try/Catch

Without centralized handling, every controller method that might fail needs its own try/catch block, each formatting errors slightly differently. This duplicates logic across the codebase and makes it easy for one endpoint's error response to drift out of sync with everyone else's — exactly the kind of inconsistency that frustrates anyone consuming your API.

@ControllerAdvice and @ExceptionHandler

@ControllerAdvice marks a class as a global handler that applies across every @RestController in your application. Inside it, each method annotated with @ExceptionHandler catches one specific exception type, letting you centralize your error-formatting logic in exactly one place.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(StudentNotFoundException.class)
public ResponseEntity<Object> handleStudentNotFound(StudentNotFoundException ex) {
Map<String, Object> body = new HashMap<>();
body.put("status", HttpStatus.NOT_FOUND.value());
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
}
}

@RestControllerAdvice is a convenience annotation combining @ControllerAdvice and @ResponseBody, so every handler method's return value is serialized straight to JSON, exactly like a @RestController.

A Custom Exception Class

Rather than returning ResponseEntity.notFound().build() scattered across every controller method, define a dedicated exception for the situation and throw it from wherever it applies.

public class StudentNotFoundException extends RuntimeException {
public StudentNotFoundException(Long id) {
super("Student not found with id: " + id);
}
}
@GetMapping("/{id}")
public ResponseEntity<Student> getStudentById(@PathVariable Long id) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new StudentNotFoundException(id));
return ResponseEntity.ok(student);
}
Example Request and Response

Click Run to see what this code prints.

Handling Validation Errors Globally

You can override Spring Boot's default validation error format by catching MethodArgumentNotValidException in the same @RestControllerAdvice class, giving every validation failure the same shape as your other errors.

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> fieldErrors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
fieldErrors.put(error.getField(), error.getDefaultMessage()));
Map<String, Object> body = new HashMap<>();
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("errors", fieldErrors);
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
Example Response

Click Run to see what this code prints.

A Consistent Error Response Shape

A common final step is adding a catch-all handler for java.lang.Exception, so any unexpected error still returns a well-formed JSON response instead of a raw stack trace or a generic Whitelabel Error Page.

@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleGeneric(Exception ex) {
Map<String, Object> body = new HashMap<>();
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("message", "An unexpected error occurred");
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
Order Matters

Spring matches the most specific exception handler available. A handler for StudentNotFoundException will always be used over a generic Exception handler when a StudentNotFoundException is thrown, even though both are technically eligible.

Common Mistakes

Avoid These Mistakes
  • Catching java.lang.Exception as the only handler, which hides more specific, useful error types behind a generic 500 response.
  • Letting sensitive details like stack traces or internal class names leak into a production error response.
  • Forgetting @RestControllerAdvice (or @ResponseBody on @ControllerAdvice), which causes Spring to try to resolve a view name instead of returning JSON.
  • Duplicating error-handling logic inside individual controllers after already centralizing it in a @ControllerAdvice class.

Best Practices

  • Centralize all exception handling in one or a small number of @RestControllerAdvice classes rather than scattering try/catch blocks.
  • Define custom exceptions for meaningful domain errors (StudentNotFoundException, DuplicateEmailException) instead of relying only on generic ones.
  • Always include a catch-all Exception handler so no unexpected error ever reaches the client as a raw stack trace.
  • Keep the error response shape consistent across every handler — the same status/message/errors structure everywhere.

Frequently Asked Questions

@RestControllerAdvice is @ControllerAdvice plus @ResponseBody applied to every handler method automatically, so return values are serialized directly to the response body as JSON — exactly what you want for a REST API.

Yes. @ExceptionHandler({TypeA.class, TypeB.class}) accepts an array, letting one method handle several related exception types the same way.

Yes, by default it applies globally across the whole application. You can scope it to specific packages or controller classes using attributes like basePackages if you need narrower coverage.

Typically from a service layer or, in simpler apps like this course's examples, directly from the controller after a failed repository lookup — anywhere business logic determines a requested resource genuinely doesn't exist.

Key Takeaways

  • @RestControllerAdvice centralizes exception handling across your entire application in one class.
  • @ExceptionHandler methods catch specific exception types and format the response for each.
  • Custom exceptions like StudentNotFoundException make controller code read clearly and keep error handling out of business logic.
  • A catch-all Exception handler ensures no unexpected error ever leaks a raw stack trace to a client.

Summary

Your API now returns consistent, well-shaped JSON errors from a single, centralized location, no matter what goes wrong or where. Next, you'll configure Spring Boot to connect to a real MySQL or PostgreSQL database, going beyond the local setup you've used so far.

Next Lesson →

Connecting to MySQL/PostgreSQL