An API that fails silently, or worse, sends back a raw stack trace, is difficult and frustrating to work with. A well-designed API tells the caller exactly what went wrong, using a clear message and the correct HTTP status code, so client applications can react sensibly instead of guessing.

Returning the right status code

Wrapping a result in ResponseEntity gives full control over the status code returned, rather than always defaulting to 200:

@GetMapping("/items/{id}")
public ResponseEntity<Item> get(@PathVariable Long id) {
  return repo.findById(id)
    .map(ResponseEntity::ok)
    .orElse(ResponseEntity.notFound().build());
}

Creating a custom exception

Rather than scattering ResponseEntity checks everywhere, many applications define their own exception types for specific problems, then throw them from deep inside service code:

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

Handling exceptions in one place

A class annotated with @RestControllerAdvice can catch exceptions thrown by any controller in the application, translating them into a consistent response format:

@RestControllerAdvice
public class ErrorHandler {
  @ExceptionHandler(NotFoundException.class)
  public ResponseEntity<String> handle(NotFoundException e) {
    return ResponseEntity.status(404).body(e.getMessage());
  }
}

With this in place, any part of the codebase can simply throw new NotFoundException("Item not found") and trust that it will turn into a proper 404 response, without repeating the same error-handling code in every controller method.

Validating input automatically

Bad input is one of the most common sources of errors, and Spring can reject it before a controller method even runs. Annotating a request body parameter with @Valid, combined with validation annotations on the class itself, triggers this automatically:

public class ProductForm {
  @NotBlank
  private String name;
  @Positive
  private double price;
}

@PostMapping("/products")
public Product create(@Valid @RequestBody ProductForm form) {
  // only reached if validation passed
}

A failed validation automatically produces a 400 Bad Request with details about which fields were invalid, all without a single explicit if statement in the controller.

A consistent error shape

Callers benefit greatly when every error, regardless of its cause, comes back in the same JSON shape, typically something like a timestamp, a message and a status field. Returning this consistent structure from the central @RestControllerAdvice class means a client only ever needs to write one piece of code to handle errors, instead of guessing the format for each different endpoint.

Why this matters

Good error handling is often invisible when everything works, but it is exactly what separates a rough prototype from an API other teams can build on with confidence, since predictable, well-described failures are far easier to debug and recover from than silent or malformed ones.