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
500handler. 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?
How do I return the correct HTTP status for an exception?
Should I expose stack traces in API error responses?
How does @RestControllerAdvice differ from @ControllerAdvice?
Where should validation errors be handled?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

