Spring Boot @ControllerAdvice — Global Exception Handling

Without centralized exception handling, every controller method ends up with try‑catch blocks, and every new exception type requires updating every controller that might throw it. @ControllerAdvice solves this by letting you define exception handling logic once, in one place, and have it apply across your entire application. It's one of those Spring features that pays off quickly once you have more than a handful of endpoints.


The Problem Without @ControllerAdvice

Here's what controller code looks like when exception handling is inline:

@GetMapping("/blogs/{id}")
public ResponseEntity<BlogStory> getBlog(@PathVariable String id) {
    try {
        BlogStory blog = blogService.findById(id);
        return ResponseEntity.ok(blog);
    } catch (NotFoundException e) {
        return ResponseEntity.notFound().build();
    } catch (AppException e) {
        return ResponseEntity.internalServerError().build();
    }
}

@PostMapping("/blogs")
public ResponseEntity<BlogStory> createBlog(@RequestBody BlogStory blog) {
    try {
        BlogStory created = blogService.create(blog);
        return ResponseEntity.status(201).body(created);
    } catch (AppException e) {  // same handler copied again
        return ResponseEntity.internalServerError().build();
    }
}

Every method that can throw AppException needs the same catch block. Add a new exception type and you're touching every controller. This is the problem @ControllerAdvice was designed to eliminate.


Custom Exception Classes

First, define the application‑specific exceptions:

public class NotFoundException extends RuntimeException {
    public NotFoundException(String message) {
        super(message);
    }
}

public class AppException extends RuntimeException {
    public AppException(String message) {
        super(message);
    }

    public AppException(String message, Throwable cause) {
        super(message, cause);
    }
}

The Global Exception Handler

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<String> handleNotFound(NotFoundException ex) {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(ex.getMessage());
    }

    @ExceptionHandler(AppException.class)
    public ResponseEntity<String> handleAppException(AppException ex) {
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body("Something went wrong: " + ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneral(Exception ex) {
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body("Unexpected error occurred");
    }
}

Each @ExceptionHandler method handles one (or more) exception types. When a controller throws NotFoundException, Spring intercepts it and routes it to handleNotFound() instead of propagating. The controller never sees the exception — it only throws it.


Clean Controllers After @ControllerAdvice

The same controller methods from before, now without any error handling code:

@RestController
@RequestMapping("/blogs")
public class BlogAPIController {

    private final BlogService blogService;

    public BlogAPIController(BlogService blogService) {
        this.blogService = blogService;
    }

    @GetMapping("/{id}")
    public BlogStory getBlog(@PathVariable String id) {
        return blogService.findById(id);  // throws NotFoundException — handled globally
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public BlogStory createBlog(@RequestBody BlogStory blog) {
        return blogService.create(blog);  // throws AppException — handled globally
    }
}

The controller is now purely about orchestration — take the request, call the service, return the result. Exception handling is a cross‑cutting concern that belongs elsewhere, and @ControllerAdvice is that elsewhere.


Returning Structured Error Responses

Returning a plain string as the error body is functional but not ideal for API clients. A structured error response is more useful:

public class ErrorResponse {
    private int status;
    private String message;
    private long timestamp;

    public ErrorResponse(int status, String message) {
        this.status = status;
        this.message = message;
        this.timestamp = System.currentTimeMillis();
    }

    // getters
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex) {
    ErrorResponse error = new ErrorResponse(404, ex.getMessage());
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

The JSON response now looks like:

{
  "status": 404,
  "message": "Blog with id 'xyz' not found",
  "timestamp": 1718123456789
}

Scoping @ControllerAdvice

By default, @ControllerAdvice applies to all controllers in the application. You can narrow its scope:

// Only applies to controllers in this package
@ControllerAdvice("com.example.api")

// Only applies to controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)

// Only applies to these specific controller classes
@ControllerAdvice(assignableTypes = {BlogAPIController.class, UserController.class})

In most cases, a single application‑wide advice class is the right approach. Multiple scoped advice classes can be useful in modular applications where different modules have different error response formats.


@RestControllerAdvice

If all your handler methods return response bodies (as in a REST API), use @RestControllerAdvice instead of @ControllerAdvice. It's the equivalent of combining @ControllerAdvice with @ResponseBody — the same way @RestController combines @Controller with @ResponseBody:

@RestControllerAdvice
public class GlobalExceptionHandler {
    // no need to wrap returns in ResponseEntity if you're always returning bodies

    @ExceptionHandler(NotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(NotFoundException ex) {
        return new ErrorResponse(404, ex.getMessage());
    }
}

Summary

@ControllerAdvice is Spring's mechanism for defining global exception handling logic that applies across all controllers. Instead of writing try‑catch blocks in every controller method, you define @ExceptionHandler methods in a single @ControllerAdvice class. Spring intercepts exceptions thrown by any controller and routes them to the appropriate handler. The result is cleaner controllers focused on business logic, consistent error responses across the API, and a single place to update when you add new exception types. Use @RestControllerAdvice for REST APIs where all responses serialise to JSON.


Happy coding!

No comments :

Post a Comment

Please leave your message queries or suggetions.

Note: Only a member of this blog may post a comment.