Skip to main content

Error Handling

Complete Code
The end result of the code developed in this document can be found in the GitHub monorepo springboot-demo-projects, under the tag normalize-errors.

Out there you are gonna come across plenty of scenarios like the following:

  • service.com/users/-1 returns:

    {
    "errorDescription": "User not found",
    "cause": "BAD REQUEST"
    }
  • but service.com/product/-1 returns:

    {
    "message": "not found",
    "error": 404
    }

Consistency just flew out of the window there, and it gets worse with errors inside 200 OK. We don’t want to be that kind of dev: we are going to do proper error handling.

Error Handling

@RestControllerAdvice acts like a central "error coordinator" for your application.

  • It's a single place where you can define how all errors, exceptions, or unexpected scenarios get translated into responses.
  • Instead of scattering error-handling logic across every controller, this tool ensures every error, whether from a user lookup, product search, or internal bug, follows the same rules and format.

Problem Details for HTTP APIs is a standardized "error template" that structures responses in a clear, consistent way. Think of it as a pre-designed form that every error fills out:

  • What type of error occurred (e.g., "film_not_found")
  • A human-readable title (e.g., "Resource Not Found")
  • The HTTP status code (e.g., 404)
  • Additional details (e.g., "Film ID -1 does not exist")

Together, these tools ensure your app never confuses clients with mismatched error formats. Even edge cases or unanticipated errors get wrapped into the same predictable structure.

Files to Create/Modify
File Tree
├── ...
└── src
├── main
│ ├── java
│ │ └── dev
│ │ └── pollito
│ │ └── spring_java
│ │ ├── config
│ │ │ ├── advice
│ │ │ │ └── ControllerAdvice.java
│ │ │ └── ...
│ │ └── ...
│ └── ...
└── ...

Create the @RestControllerAdvice class

java/dev/pollito/spring_java/config/advice/ControllerAdvice.java
package dev.pollito.spring_java.config.advice;

import static io.opentelemetry.api.trace.Span.current;
import static java.time.Instant.*;
import static java.time.format.DateTimeFormatter.ISO_INSTANT;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.NOT_FOUND;

import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;

@RestControllerAdvice
@Slf4j
public class ControllerAdvice {
private static @NotNull ProblemDetail buildProblemDetail(
@NotNull Exception e, @NotNull HttpStatus status) {
String exceptionSimpleName = e.getClass().getSimpleName();
String logMessage = "{} being handled";

switch (status.series()) {
case SERVER_ERROR -> log.error(logMessage, exceptionSimpleName, e);
case CLIENT_ERROR -> log.warn(logMessage, exceptionSimpleName, e);
default -> log.info(logMessage, exceptionSimpleName, e);
}

ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, e.getLocalizedMessage());
problemDetail.setProperty("timestamp", ISO_INSTANT.format(now()));
problemDetail.setProperty("trace", current().getSpanContext().getTraceId());

return problemDetail;
}

@ExceptionHandler(Exception.class)
public ProblemDetail handle(Exception e) {
return buildProblemDetail(e, INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(NoResourceFoundException.class)
public ProblemDetail handle(NoResourceFoundException e) {
return buildProblemDetail(e, NOT_FOUND);
}
}

If you visit a uri that doesn't exist (like http://localhost:8080), you will now get a standardized error:

Terminal
curl -s http://localhost:8080 | jq; curl -sw "→ HTTP %{http_code}\n" -o /dev/null http://localhost:8080
{
"detail": "No static resource for request '/'.",
"instance": "/",
"status": 404,
"title": "Not Found",
"timestamp": "2026-01-11T20:16:13.240960834Z",
"trace": "d9178227-18d6-4442-8598-9a9f17f65f9c"
}
→ HTTP 404

Here's what happens under the hood:

Scroll to zoom • Drag corner to resize

Common Handlers You May Need

If an exception occurs that isn't handled by any specific @ExceptionHandler in your @RestControllerAdvice, it will fall through to the default @ExceptionHandler(Exception.class) which returns a generic 500 Internal Server Error response.

Here are the most common exceptions you'll want to handle explicitly:

ExceptionDescriptionExampleNotes
ConstraintViolationExceptionRequest parameters/fields fail validation (@NotNull, @Size, @Pattern)Request body missing a required fieldRequires Jakarta EE (to be added later)
MethodArgumentTypeMismatchExceptionRequest parameter cannot be converted to expected typeController expects Integer but receives String
NoResourceFoundExceptionRequest accesses non-existent Spring MVC resourceAccessing an undefined endpoint
NoSuchElementExceptionOptional.get() called on empty OptionalLooking for non-existent user by ID
PropertyReferenceExceptionInvalid property used in Spring Data repository querySorting by non-existent fieldRequires Spring Data (to be added later)