Spring BootREST APIsbeginner
Updated:

Exception Handling in Spring Boot REST APIs with @ControllerAdvice

5 min read

How to turn raw stack traces into clean, consistent JSON error responses using @ExceptionHandler and a global @ControllerAdvice handler.

TL;DR – Quick Answer

Exception handling in Spring Boot REST APIs means catching errors and returning a consistent JSON response with the right HTTP status instead of leaking a stack trace. You define a global handler class annotated with @RestControllerAdvice and add @ExceptionHandler methods that map each exception type to a status code and error body. This centralises error handling so your controllers stay focused on the happy path.

On This Page

The difference between a toy API and a professional one is often visible only when something goes wrong. A toy API returns a 500 with a wall of Java stack trace; a professional one returns a clean 404 with {"status":404,"message":"Order 42 not found"}. This tutorial shows how to make every error in your Spring Boot API look like the second example, using @ExceptionHandler and a global @RestControllerAdvice.

Why default error handling is not enough

Out of the box, Spring Boot catches unhandled exceptions and returns a generic error response — useful for a quick start, but wrong for a real API. Two problems stand out. First, the status code is often 500 even when the real cause is a missing resource that deserves 404 or bad input that deserves 400. Second, the response shape is inconsistent and may leak internals.

Good error handling fixes both: every exception maps to the correct HTTP status, and every error body follows the same structure. Clients can then handle errors programmatically instead of parsing prose. This complements the happy-path work in building a REST API with Spring Boot.

Start with meaningful custom exceptions

Generic exceptions produce generic responses. Define exceptions that carry business meaning:

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

Now a service can throw it with a precise message:

@Service
public class OrderService {
    public Order findById(Long id) {
        return repository.findById(id)
            .orElseThrow(() ->
                new ResourceNotFoundException("Order " + id + " not found"));
    }
}

Extending RuntimeException keeps the method signatures clean — you do not have to declare throws up the chain. The exception name itself communicates intent, which makes the handler code that follows read naturally.

A consistent error response shape

Decide on one structure for every error and use it everywhere. A simple record works well:

import java.time.Instant;

public record ApiError(
        Instant timestamp,
        int status,
        String error,
        String message) {
}

Every error your API returns will now have a timestamp, a numeric status, a short error label, and a human-readable message. Consistency here is what lets a frontend written by a teammate — perhaps the one working through connecting React to a Spring Boot backend — handle all failures with one code path.

Handling one controller with @ExceptionHandler

You can handle exceptions inside a single controller by adding an @ExceptionHandler method:

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    public Order get(@PathVariable Long id) {
        return orderService.findById(id); // may throw ResourceNotFoundException
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex) {
        ApiError body = new ApiError(
            Instant.now(), 404, "Not Found", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
    }
}

This works, but the handler only applies to OrderController. If ten controllers can throw ResourceNotFoundException, you would copy this method ten times. That duplication is exactly what global handling removes.

The global handler with @RestControllerAdvice

Move the handlers into one class annotated with @RestControllerAdvice and they apply to every controller in the application:

import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex) {
        return build(HttpStatus.NOT_FOUND, ex.getMessage());
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ApiError> handleBadInput(IllegalArgumentException ex) {
        return build(HttpStatus.BAD_REQUEST, ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiError> handleAny(Exception ex) {
        // log the full stack trace here, return a safe message
        return build(HttpStatus.INTERNAL_SERVER_ERROR,
            "An unexpected error occurred");
    }

    private ResponseEntity<ApiError> build(HttpStatus status, String message) {
        ApiError body = new ApiError(
            Instant.now(), status.value(), status.getReasonPhrase(), message);
        return ResponseEntity.status(status).body(body);
    }
}

Now every controller gets the same behaviour with zero duplication. ResourceNotFoundException becomes a 404, bad input a 400, and anything unexpected a 500 with a safe message while the real cause is logged. @RestControllerAdvice is @ControllerAdvice plus @ResponseBody, so the returned objects serialise straight to JSON — a distinction worth knowing from the Spring Boot annotations tutorial.

Pro tip: Order your handlers from most specific to most general, and always include a catch-all @ExceptionHandler(Exception.class). The catch-all is your safety net: it guarantees no raw stack trace ever escapes to a client, even for exceptions you did not anticipate.

Common mistake: Returning the exception's message directly from the catch-all 500 handler. That message may contain SQL fragments, file paths or class names. Log the full detail server-side and return a generic sentence to the client. Never let internal text leak through the top-level handler.

Handling validation failures the same way

When you add request validation, a failed check throws MethodArgumentNotValidException. Handle it in the same advice class so validation errors match the rest of your API:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidation(
        MethodArgumentNotValidException ex) {
    String message = ex.getBindingResult().getFieldErrors().stream()
        .map(f -> f.getField() + ": " + f.getDefaultMessage())
        .reduce((a, b) -> a + ", " + b)
        .orElse("Validation failed");
    return build(HttpStatus.BAD_REQUEST, message);
}

This gathers the field-level messages into a single 400 response with the same ApiError shape. Validation and error handling are two sides of the same coin: validation decides what is wrong, the handler decides how the client hears about it.

ResponseStatusException for quick cases

Not every error deserves its own exception class. For simple, one-off cases Spring offers ResponseStatusException, which lets you set the status and message inline without a handler:

@GetMapping("/{id}")
public Order get(@PathVariable Long id) {
    return repository.findById(id)
        .orElseThrow(() -> new ResponseStatusException(
            HttpStatus.NOT_FOUND, "Order " + id + " not found"));
}

This is convenient for small controllers, but it has a cost: the status is now decided inside the controller rather than in one central place, and the error body shape is Spring's default rather than your consistent ApiError. Use ResponseStatusException for quick internal endpoints, and custom exceptions with a global advice for any API that external clients depend on. Knowing when to reach for each is a sign of judgement rather than dogma.

The modern ProblemDetail format

Recent Spring versions support ProblemDetail, a standardised error format defined by RFC 9457 (formerly 7807). Instead of inventing your own error shape, you return a well-known structure with fields like type, title, status and detail:

@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.NOT_FOUND, ex.getMessage());
    problem.setTitle("Resource Not Found");
    return problem;
}

The advantage is interoperability: clients and API gateways that understand the standard can parse your errors without custom code. For a new API, ProblemDetail is worth adopting because it gives you the consistency of a custom ApiError while following a format the wider ecosystem already recognises. Whichever shape you choose, the golden rule holds — pick one and use it everywhere, so that every error your Spring Boot API returns is predictable.

Interview relevance

REST error handling is a favourite scenario question because it reveals whether you have built a real API. Be ready to explain the difference between a local @ExceptionHandler and a global @RestControllerAdvice, why you map exceptions to specific HTTP statuses, and why you never expose stack traces. Mention the catch-all handler as your safety net and the consistent error body as what makes an API easy to consume. Practise these alongside the Spring Boot REST API interview questions, where "how do you handle errors in a REST API" is almost always asked. This is a core skill we drill in the Java Full Stack program because it separates production-ready code from demos.

Frequently Asked Questions

What is the difference between @ExceptionHandler and @ControllerAdvice?
@ExceptionHandler on a controller method handles exceptions only for that single controller. @ControllerAdvice (or @RestControllerAdvice) puts @ExceptionHandler methods in one class that applies across all controllers. Use the local form for one-off cases and the global advice for consistent, application-wide error handling.
How do I return the correct HTTP status for an exception?
Return a ResponseEntity with the desired HttpStatus from the handler method, or annotate the handler with @ResponseStatus. For a not-found error you return 404, for a validation error 400, and for an unexpected failure 500. Mapping each exception type to a meaningful status is central to a well-behaved REST API.
Should I expose stack traces in API error responses?
No. Stack traces leak internal implementation details and can reveal security-sensitive information. Return a clean error body with a message, status and timestamp, and log the full stack trace on the server instead. Clients get a useful message while the details stay in your logs.
How does @RestControllerAdvice differ from @ControllerAdvice?
@RestControllerAdvice is @ControllerAdvice combined with @ResponseBody, so its handler methods return objects serialised directly to JSON. Plain @ControllerAdvice would try to resolve a view name unless you add @ResponseBody yourself. For REST APIs, @RestControllerAdvice is the convenient choice.
Where should validation errors be handled?
Bean Validation failures surface as MethodArgumentNotValidException, which you handle in your @RestControllerAdvice like any other exception, usually returning a 400 with the field errors. This keeps validation error formatting consistent with the rest of your API error handling.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 15 July 2026 LinkedIn
Chat with us