package com.example.api.error;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.Instant;
import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ApiError(
Instant timestamp,
int status,
String error,
String message,
String path,
List<FieldError> fieldErrors
) {
public record FieldError(String field, String message) {
}
public static ApiError of(int status, String error, String message, String path) {
return new ApiError(Instant.now(), status, error, message, path, null);
}
public static ApiError of(int status, String error, String message, String path, List<FieldError> fieldErrors) {
return new ApiError(Instant.now(), status, error, message, path, fieldErrors);
}
}
package com.example.api.error;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.util.List;
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex, HttpServletRequest request) {
return build(HttpStatus.NOT_FOUND, ex.getMessage(), request);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiError> handleBadRequest(IllegalArgumentException ex, HttpServletRequest request) {
return build(HttpStatus.BAD_REQUEST, ex.getMessage(), request);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
List<ApiError.FieldError> fields = ex.getBindingResult().getFieldErrors().stream()
.map(fe -> new ApiError.FieldError(fe.getField(), fe.getDefaultMessage()))
.toList();
String path = request.getDescription(false).replaceFirst("^uri=", "");
ApiError body = ApiError.of(HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
"Validation failed", path, fields);
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleUnexpected(Exception ex, HttpServletRequest request) {
log.error("Unhandled exception at {}", request.getRequestURI(), ex);
return build(HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred", request);
}
private ResponseEntity<ApiError> build(HttpStatus status, String message, HttpServletRequest request) {
ApiError body = ApiError.of(status.value(), status.getReasonPhrase(), message, request.getRequestURI());
return ResponseEntity.status(status).body(body);
}
}
package com.example.api.order;
import com.example.api.error.ResourceNotFoundException;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
public OrderResponse getOrder(@PathVariable Long id) {
return orderService.find(id)
.orElseThrow(() -> new ResourceNotFoundException("Order " + id + " not found"));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderResponse create(@Valid @RequestBody CreateOrderRequest request) {
return orderService.create(request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void cancel(@PathVariable Long id) {
if (!orderService.cancel(id)) {
throw new ResourceNotFoundException("Order " + id + " not found");
}
}
}
This snippet shows how a Spring Boot API centralizes error handling so that every failure — validation, missing entities, or unexpected exceptions — returns a consistent JSON body instead of the framework's default Whitelabel page. Scattering try/catch blocks across controllers is repetitive and produces inconsistent responses; a single @RestControllerAdvice lets the mapping from exception type to HTTP status and payload live in one place.
The ApiError response tab defines the wire format. It is an immutable value object carrying a timestamp, the numeric status, the reason error, a human message, the request path, and an optional list of fieldErrors. The nested FieldError record holds a field/message pair. Marking the class with @JsonInclude(NON_NULL) keeps the fieldErrors key out of the JSON when there are no validation problems, so a simple 404 stays lean while a 400 can enumerate every bad field.
The GlobalExceptionHandler advice tab is the core. It extends ResponseEntityExceptionHandler so Spring's own handlers (for things like unreadable JSON) can be overridden with the same shape. Each @ExceptionHandler method maps a specific exception to a status: ResourceNotFoundException becomes 404, and MethodArgumentNotValidException becomes 400. The validation handler walks ex.getBindingResult().getFieldErrors() and projects each into an ApiError.FieldError, giving clients machine-readable detail about which inputs failed. A catch-all Exception handler returns 500 and deliberately logs the stack trace while returning a generic message, so internal details never leak to callers. The private build helper stamps the current path from the HttpServletRequest and keeps every response uniform.
The OrderController tab demonstrates the payoff. Its methods stay focused on the happy path: getOrder simply throws ResourceNotFoundException when the service returns nothing, and create relies on @Valid to trigger bean validation. Neither method contains error-formatting code — the advice intercepts thrown exceptions after the controller returns. A key pitfall to remember is scope: @RestControllerAdvice only intercepts exceptions raised within the DispatcherServlet, so errors thrown from filters or during security authentication need separate handling. Ordering also matters, since the most specific @ExceptionHandler wins over the broad Exception fallback.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
Share this code
Here's the card — post it anywhere.