RizTech Academy logo
RizTech Academy
Building REST APIsLesson 5 of 635 min

Global error handling and consistent error responses

When something goes wrong — a parcel is not found, input is invalid, a business rule is violated — your API must return a sensible, consistent error the client can handle. Scatter try/catch and manual error responses through your controllers and you get inconsistency and noise. Spring's answer is @RestControllerAdvice: one place that turns exceptions into HTTP responses, so your controllers throw and stay clean. This lesson builds Dakiya's global error handling, verified against the running API.

The problem: error handling scattered in controllers

Without a strategy, every controller method handles its own errors:

@GetMapping("/{id}")
public ResponseEntity<?> getOne(@PathVariable Long id) {
    Optional<Parcel> p = repository.findById(id);
    if (p.isEmpty()) {
        return ResponseEntity.status(404).body(Map.of("error", "not found"));  // repeated everywhere
    }
    return ResponseEntity.ok(ParcelResponse.from(p.get()));
}

Every method repeats the not-found handling, error shapes drift (one returns {"error":...}, another {"message":...}), and the happy-path logic is buried in error checks. This does not scale.

The pattern: throw, and handle centrally

The clean approach: controllers throw an exception when something is wrong, and one central handler turns each exception type into a response. The controller becomes simple:

@GetMapping("/{id}")
public ParcelResponse getOne(@PathVariable Long id) {
    Parcel p = repository.findById(id)
        .orElseThrow(() -> new NotFoundException("No parcel with id " + id));   // just throw
    return ParcelResponse.from(p);
}

With a small exception type:

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

The controller now expresses only the happy path plus "throw if not found" — no status codes, no error bodies. Where do those come from? The advice.

@RestControllerAdvice: one handler for the whole app

A class annotated @RestControllerAdvice contains @ExceptionHandler methods that catch exceptions from any controller and turn them into responses:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NotFoundException.class)
    ProblemDetail handleNotFound(NotFoundException ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)   // validation failures (previous lesson)
    ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors()
          .forEach(fe -> errors.put(fe.getField(), fe.getDefaultMessage()));
        pd.setProperty("errors", errors);
        return pd;
    }
}

Each @ExceptionHandler maps an exception type to a response. When a controller throws NotFoundException, Spring finds the matching handler and returns its result. Verified against the running API: GET /api/parcels/999 (no such parcel) returned HTTP 404 with body {"type":"about:blank","title":"Not Found","status":404,"detail":"No parcel with id 999","instance":"/api/parcels/999"}, and an invalid POST returned the 400 with the field-errors map. Every controller in the app gets this handling for free — throw the exception, the advice shapes the response.

ProblemDetail: the standard error format

Notice the handlers return ProblemDetail. This is Spring's built-in implementation of RFC 9457 (Problem Details for HTTP APIs) — a standard JSON error shape with type, title, status, detail, and instance, plus your own properties (like the errors map). Using the standard format matters: clients and tools understand it, and every error in your API looks the same, so a client writes one piece of error-handling code. Verified: both the 404 and the 400 came back as ProblemDetail-shaped JSON with the consistent fields. You can add custom fields with pd.setProperty(...) (as the validation errors map shows). Prefer ProblemDetail over hand-rolled error objects — it is the modern Spring standard and gives you consistency for free.

Mapping the right status to the right error

Good error handling means the right HTTP status for each situation — this is a REST contract, not decoration:

  • 400 Bad Request — malformed or invalid input (validation failures, unparseable body).
  • 401 Unauthorized — not authenticated (the security module).
  • 403 Forbidden — authenticated but not allowed.
  • 404 Not Found — the resource does not exist (Dakiya's NotFoundException).
  • 409 Conflict — a conflict with current state (a duplicate, a violated unique constraint).
  • 422 Unprocessable Entity — well-formed but semantically invalid (sometimes used instead of 400 for business-rule failures).
  • 500 Internal Server Error — an unexpected failure (a bug); Spring returns this for uncaught exceptions.

Map each of your exception types to the status that honestly describes what happened — a not-found is 404, not 500; invalid input is 400, not 200-with-an-error-in-the-body. A catch-all handler for Exception can return a generic 500 (and log it, per the errors-and-logging lesson) so an unexpected bug does not leak a stack trace to the client. The principle: controllers throw meaningful exceptions; one advice maps each to the correct status and a consistent ProblemDetail — clean controllers, consistent errors, right status codes.

Check your work

The problem. Per-controller error handling repeats itself, drifts in shape, and buries the happy path.

The pattern. Controllers throw meaningful exceptions (e.g. NotFoundException); a central handler maps them to responses. Controllers stay simple.

@RestControllerAdvice. A class of @ExceptionHandler methods catching exceptions from all controllers; each maps an exception type to a response. Verified: NotFoundException → 404, validation → 400 with errors, app-wide.

ProblemDetail. Spring's RFC 9457 standard error format (type/title/status/detail/instance + custom properties) — consistent, tool-understood errors; prefer it over hand-rolled error objects. Verified shape on both 404 and 400.

Right status codes. 400 (invalid input), 401/403 (auth), 404 (not found), 409 (conflict), 422 (semantic), 500 (unexpected) — map each exception to the honest status; a catch-all 500 handler (with logging) prevents leaking stack traces.

Practice

  1. Create a NotFoundException and throw it from a GET when the parcel is absent (orElseThrow); confirm the controller has no error-handling code.
  2. Add a @RestControllerAdvice with an @ExceptionHandler(NotFoundException.class) returning a 404 ProblemDetail; hit a missing id and confirm the verified 404 body.
  3. Add the validation handler; POST invalid input and confirm the 400 with the field-errors map.
  4. Add a second controller that also throws NotFoundException and confirm it gets the same handling for free (app-wide advice).
  5. Add a catch-all @ExceptionHandler(Exception.class) returning 500; trigger an unexpected error and confirm no stack trace leaks to the client (and that it is logged).
  6. For five error situations in Dakiya, choose the correct HTTP status and justify each.

Official documentation

Next: REST API design — status codes, versioning, pagination.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship