Exception Handling Like a Pro in Spring Boot REST APIs

In a perfect world, your REST API always works flawlessly. But in the real world? Things break.

How your application handles those failures can define the difference between a developer’s dream and a support team’s nightmare.

In this post, you’ll learn how to implement robust, consistent, and professional-grade exception handling in Spring Boot. We’ll go beyond catching errors—we’ll turn exceptions into structured, debuggable, and meaningful responses.

You’ll walk away knowing:

  • Why default exception handling isn’t enough
  • How to use @ControllerAdvice and @ExceptionHandler effectively
  • How to structure standardized error responses
  • What to log—and what not to expose
  • Strategies to keep your responses useful in production and in development

Why You Should Care About Exception Handling

Imagine your API throws a NullPointerException, and the client receives a 500 error with this message:

Internal Server Error

That’s not just unhelpful—it’s dangerous. It tells the client nothing, hides the actual cause, and makes debugging a guessing game.

Now imagine instead they get:

{
  "timestamp": "2025-06-16T09:30:00Z",
  "status": 400,
  "error": "Bad Request",
  "message": "User ID must not be null",
  "path": "/api/users"
}

Suddenly, your API feels like it’s built by pros.

The Default Behavior in Spring Boot

Out of the box, Spring Boot does handle exceptions—sort of. If you throw an exception like IllegalArgumentException, you’ll get a generic response with a stack trace in the dev environment and a blank 500 error in production.

But if you want control over the response format, HTTP status codes, and the message’s clarity—you’ll need to override this.

Introducing @ControllerAdvice and @ExceptionHandler

Spring gives you the perfect tools: @ControllerAdvice and @ExceptionHandler.

Let’s start with a simple global exception handler.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) {
        return new ErrorResponse(
            Instant.now(),
            HttpStatus.BAD_REQUEST.value(),
            "Bad Request",
            ex.getMessage(),
            request.getRequestURI()
        );
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGenericException(Exception ex, HttpServletRequest request) {
        return new ErrorResponse(
            Instant.now(),
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Internal Server Error",
            "An unexpected error occurred",
            request.getRequestURI()
        );
    }
}

And here’s the ErrorResponse class:

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

This structure is clean, informative, and ready to be extended.

Comparsion Between Default and Customized Exceptions Handling

Custom Exceptions Are Your Best Friends

Let’s say you have a use case where a user isn’t found. Throwing a RuntimeException is vague and unprofessional. Instead:

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(Long userId) {
        super("User with ID " + userId + " not found");
    }
}

And handle it specifically:

@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleUserNotFound(UserNotFoundException ex, HttpServletRequest request) {
    return new ErrorResponse(
        Instant.now(),
        HttpStatus.NOT_FOUND.value(),
        "Not Found",
        ex.getMessage(),
        request.getRequestURI()
    );
}

Avoid Leaking Sensitive Info

A common mistake: returning exception stack traces or internal error messages to clients.

Bad:

{
  "message": "org.hibernate.exception.SQLGrammarException: syntax error at or near \\"DROP\\""
}

This exposes internal tech and risks security.

Good:

{
  "message": "An error occurred while processing your request. Please try again later."
}

Log the details. Don’t expose them.

Strategy for Logging vs. Client Messaging

EnvironmentClient MessageLog Message
DevFull exception messageFull stack trace
ProductionGeneric user-friendly messageDetailed exception with request ID

Tip: Add a traceId to every error response and correlate it with logs for better debugging.

Bonus: Return Validation Errors Nicely

Spring can handle @Valid and return a list of errors, but you can customize it:

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationErrorResponse handleValidationErrors(MethodArgumentNotValidException ex) {
    List<FieldError> errors = ex.getBindingResult().getFieldErrors();

    List<FieldValidationError> fieldErrors = errors.stream()
        .map(err -> new FieldValidationError(err.getField(), err.getDefaultMessage()))
        .toList();

    return new ValidationErrorResponse(
        Instant.now(),
        HttpStatus.BAD_REQUEST.value(),
        "Validation Failed",
        fieldErrors
    );
}
public record FieldValidationError(String field, String message) {}

public record ValidationErrorResponse(
    Instant timestamp,
    int status,
    String error,
    List<FieldValidationError> errors
) {}

Common Spring Exceptions and Recommended HTTP Codes Cheat Sheet


Keep It Clean with Layers

  • Use service-layer exceptions (UserNotFoundException, InsufficientFundsException)
  • Catch and convert low-level exceptions into these domain-specific ones
  • Let the @ControllerAdvice translate them to meaningful responses

Final Thoughts: Build for Devs, Protect for Users

Well-structured error handling isn’t just a technical detail. It’s a signal of quality. It improves:

  • Developer productivity during integration
  • Client trust when things go wrong
  • Production observability with traceability and structured logs

You’re not just handling errors. You’re designing a safer, clearer, and more professional API.

✅ TL;DR Checklist

  • [ ] Use @ControllerAdvice and @ExceptionHandler
  • [ ] Create structured error response DTOs
  • [ ] Avoid leaking internal info
  • [ ] Add traceId for observability
  • [ ] Customize validation error outputs
  • [ ] Map domain exceptions to HTTP status codes

🔗 Want More?

If you liked this post, you’ll love my eBook on Mastering Spring Boot Project Structure—with real-world organization tips for scalable, professional APIs.

Leave a Reply

Your email address will not be published. Required fields are marked *