Exception Handling Interview Questions & Answers
Exception handling in Spring MVC — @ExceptionHandler, @RestControllerAdvice, @ResponseStatus, ResponseEntityExceptionHandler, ProblemDetail (RFC 7807), the default error page, mapping exceptions to status codes, and consistent API error responses.
@ExceptionHandler marks a method that handles exceptions thrown by handler methods in the
same controller. When a matching exception bubbles up, Spring routes it to that method instead
of letting it become a generic 500.
@RestController
class OrderController {
@GetMapping("/orders/{id}")
Order get(@PathVariable Long id) {
return service.find(id); // may throw OrderNotFoundException
}
@ExceptionHandler(OrderNotFoundException.class)
ResponseEntity<ApiError> handle(OrderNotFoundException ex) {
return ResponseEntity.status(404).body(new ApiError(ex.getMessage()));
}
}
The method can take the exception (and request/response) as parameters and return any normal
handler return type — a ResponseEntity, a body with @ResponseStatus, etc. Defined inside a
controller it's local; moved to an advice class it becomes global.
Rule of thumb: @ExceptionHandler turns a thrown exception into a proper HTTP response;
keep it local for controller-specific errors, global for shared ones.
@RestControllerAdvice is a global exception-handling component — @ControllerAdvice +
@ResponseBody — whose @ExceptionHandler methods apply to every controller in the app.
It centralizes error handling in one place.
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
ResponseEntity<ApiError> notFound(EntityNotFoundException ex) {
return ResponseEntity.status(404).body(new ApiError("NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(Exception.class) // catch-all fallback
ResponseEntity<ApiError> generic(Exception ex) {
log.error("Unhandled", ex);
return ResponseEntity.status(500).body(new ApiError("INTERNAL", "Unexpected error"));
}
}
One advice class gives every endpoint a consistent error shape, removes try/catch noise
from controllers, and is the right place to log unexpected failures. You can scope it to
certain packages or annotations with attributes like basePackages.
Rule of thumb: Put one @RestControllerAdvice at the root of your API for uniform,
DRY error responses; keep controllers focused on the happy path.
Annotating an exception class with @ResponseStatus tells Spring which HTTP status to
return when that exception escapes a handler — no @ExceptionHandler needed for the status.
@ResponseStatus(HttpStatus.NOT_FOUND) // any escape → 404
class OrderNotFoundException extends RuntimeException {
OrderNotFoundException(Long id) { super("Order " + id + " not found"); }
}
// thrown anywhere:
throw new OrderNotFoundException(42L); // client gets 404
It's the lightest-weight mapping for a custom exception, great for simple "this maps to that
code" cases. The downside: you don't control the response body shape this way, and the
status is baked into the exception type. For a structured body or per-call logic, use an
@ExceptionHandler instead.
Rule of thumb: @ResponseStatus on the exception for a quick status mapping; switch to
@ExceptionHandler when you need a custom body.
Boot's BasicErrorController catches anything that reaches the container and renders a
default error response — a JSON body for API clients, the "Whitelabel" HTML page for browsers.
// GET /orders/999 that throws, with no custom handling:
{
"timestamp": "2026-06-26T10:00:00.000+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/orders/999"
}
The status comes from the exception (via @ResponseStatus) or defaults to 500. You can tune
what's included with server.error.include-message and include-stacktrace, replace the page,
or — better — register a @RestControllerAdvice so you never fall back to the default. Never
enable stack traces in production; they leak internals.
Rule of thumb: The Whitelabel/BasicErrorController is the safety net; in real APIs,
override it with a global advice and keep stack traces out of responses.
Spring picks the most specific exception handler by class hierarchy, and a controller- local handler beats a global advice one for the same type.
@RestControllerAdvice
class Advice {
@ExceptionHandler(RuntimeException.class) RE handleRE(...) { ... } // broad
@ExceptionHandler(IllegalStateException.class) ISE handleISE(...) { } // narrower → wins
}
// Throwing IllegalStateException → handleISE (closest match)
// Throwing NullPointerException → handleRE (nearest ancestor present)
Resolution: first a matching handler in the throwing controller, then matching handlers in
@ControllerAdvice beans (ordered by @Order), always choosing the nearest supertype of
the thrown exception. A local handler shadows a global one, which is how you special-case one
controller while keeping a global default.
Rule of thumb: Most specific exception type wins, and controller-local beats global —
order your advice with @Order if you have overlapping catch-all handlers.
ResponseEntityExceptionHandler is a base class for @ControllerAdvice that already has
@ExceptionHandlers for Spring MVC's built-in exceptions (validation, unreadable body,
missing param, method not supported…). You extend it and override hooks to customize them.
@RestControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders h,
HttpStatusCode status, WebRequest req) {
var errors = ex.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage()).toList();
return ResponseEntity.badRequest().body(new ApiError("VALIDATION", errors));
}
}
Extending it means you get a head start on all the framework exceptions and only override the
ones whose body you care about, while still adding your own @ExceptionHandlers for domain
exceptions. In recent Spring versions these defaults already produce ProblemDetail bodies.
Rule of thumb: Extend ResponseEntityExceptionHandler to reshape Spring's built-in MVC
exceptions consistently, then add domain handlers on top.
ProblemDetail is Spring 6 / Boot 3's built-in implementation of RFC 7807 "Problem Details
for HTTP APIs" — a standardized JSON error format with fields like type, title,
status, detail, and instance.
@ExceptionHandler(OrderNotFoundException.class)
ProblemDetail handle(OrderNotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
pd.setType(URI.create("https://api.example.com/errors/order-not-found"));
pd.setTitle("Order not found");
pd.setProperty("orderId", ex.getId()); // custom extension member
return pd; // serialized as application/problem+json
}
Returning a ProblemDetail gives clients a machine-readable, standardized error envelope
instead of a bespoke shape, with Content-Type: application/problem+json. You can also enable
it globally for Spring's built-in exceptions via
spring.mvc.problemdetails.enabled=true.
Rule of thumb: Prefer ProblemDetail (RFC 7807) for new APIs — it's the standard error
envelope, supported natively in Boot 3, and saves you inventing your own.
Define one error model, return it from a single global advice, and map each exception to a meaningful status + machine-readable code. Consistency lets clients handle errors generically.
record ApiError(String code, String message, Instant timestamp, List<String> details) {}
@RestControllerAdvice
class GlobalHandler {
@ExceptionHandler(EntityNotFoundException.class)
ResponseEntity<ApiError> notFound(EntityNotFoundException ex) {
return build(HttpStatus.NOT_FOUND, "NOT_FOUND", ex.getMessage(), List.of());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<ApiError> invalid(MethodArgumentNotValidException ex) {
var d = ex.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + " " + f.getDefaultMessage()).toList();
return build(HttpStatus.BAD_REQUEST, "VALIDATION", "Invalid request", d);
}
}
Include a stable code clients can switch on (separate from the human message), the
right status, and a details list for field errors. ProblemDetail is a good ready-made
shape. Keep internal exception messages and stack traces out of it.
Rule of thumb: One error model + one global advice + a stable machine-readable code per error = an API clients can handle programmatically.
Favor unchecked (RuntimeException) subclasses for domain errors. Spring's whole stack —
including @Transactional rollback rules — is built around runtime exceptions, and they keep
signatures clean.
class OrderNotFoundException extends RuntimeException { ... } // domain error, unchecked
class PaymentDeclinedException extends RuntimeException { ... }
@Service
class OrderService {
Order find(Long id) {
return repo.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id)); // no throws clause needed
}
}
Checked exceptions force throws declarations up the call chain and clutter lambdas/streams.
Note that by default @Transactional rolls back on unchecked exceptions but commits on
checked ones unless you set rollbackFor — another reason unchecked domain exceptions are
the path of least surprise.
Rule of thumb: Model domain failures as unchecked exceptions and map them to status codes
in a @RestControllerAdvice; reserve checked exceptions for truly recoverable conditions.
Handle MethodArgumentNotValidException (for @Valid @RequestBody) and
ConstraintViolationException (for @Validated params) in your advice and flatten the
field errors into a list.
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<ApiError> onBodyInvalid(MethodArgumentNotValidException ex) {
Map<String,String> fields = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField,
FieldError::getDefaultMessage, (a, b) -> a));
return ResponseEntity.badRequest()
.body(new ApiError("VALIDATION_FAILED", "Request has invalid fields", fields));
}
MethodArgumentNotValidException carries a BindingResult with every FieldError;
ConstraintViolationException carries ConstraintViolations for path/query params. Returning
all field errors at once (not just the first) is far friendlier to clients than failing
fast on one.
Rule of thumb: Catch MethodArgumentNotValidException/ConstraintViolationException,
collect every field error into a map/list, and return them together as a 400.
Log by severity: expected client errors (4xx) at WARN or not at all, unexpected server
errors (5xx) at ERROR with the stack trace. Don't log a full trace for every 404.
@ExceptionHandler(OrderNotFoundException.class)
ResponseEntity<ApiError> notFound(OrderNotFoundException ex) {
log.warn("Order not found: {}", ex.getMessage()); // no stack trace — it's expected
return ResponseEntity.status(404).body(ApiError.of("NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(Exception.class)
ResponseEntity<ApiError> unexpected(Exception ex) {
log.error("Unhandled exception", ex); // full stack trace — investigate
return ResponseEntity.status(500).body(ApiError.of("INTERNAL", "Unexpected error"));
}
A 404 or validation error is normal traffic — logging stack traces for it buries the real
problems. Reserve ERROR + stack trace for genuine bugs (the catch-all 500). Add a
correlation/trace id so a logged error can be tied to the response a client saw.
Rule of thumb: WARN (no trace) for expected 4xx, ERROR (with trace) for 5xx; attach a correlation id so support can match a log line to a client's failure.
No. @ExceptionHandler/@ControllerAdvice only catch exceptions thrown during MVC
handler invocation. Errors in @Async methods, in filters (before dispatch), or in
background threads bypass it entirely.
// @Async exceptions: handle with a dedicated handler, not @ControllerAdvice
@Configuration
class AsyncConfig implements AsyncConfigurer {
@Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> log.error("Async failure in {}", method, ex);
}
}
For a Future/CompletableFuture return type the exception surfaces to the caller; for a
void @Async method it goes to the AsyncUncaughtExceptionHandler. Filter exceptions occur
outside the DispatcherServlet, so they hit the container's error handling — catch them in the
filter or with an error-dispatch mapping.
Rule of thumb: @ControllerAdvice covers only MVC handler execution; wire an
AsyncUncaughtExceptionHandler for @Async and handle filter errors inside the filter.
Often yes — translate infrastructure exceptions into meaningful domain exceptions at the service boundary, so controllers and advice deal in business terms, not JDBC/HTTP details.
@Service
class PaymentService {
Receipt charge(Card card, Money amount) {
try {
return gateway.charge(card, amount);
} catch (GatewayTimeoutException ex) {
throw new PaymentUnavailableException("Payment provider timed out", ex); // wrap
}
}
}
Always pass the original as the cause so the stack trace is preserved. Spring itself does
this — @Repository translates vendor SQL exceptions into the DataAccessException hierarchy.
Don't translate blindly, though: wrapping that loses information or buries a bug is worse than
letting it propagate to the catch-all handler.
Rule of thumb: Translate low-level exceptions into domain ones at the boundary (keeping the cause), so your error model stays in business language — but never swallow the original.
A request that matches no mapping doesn't throw a normal handler exception by default — it
hits the error controller. To handle it in your advice, make Spring throw
NoHandlerFoundException and catch it.
spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=false # so static handler doesn't swallow it
@ExceptionHandler(NoHandlerFoundException.class)
ResponseEntity<ApiError> noHandler(NoHandlerFoundException ex) {
return ResponseEntity.status(404)
.body(new ApiError("NO_ROUTE", "No endpoint " + ex.getRequestURL()));
}
Without those properties the default static-resource handler matches everything and you never see the exception, so the Whitelabel 404 wins. With them on, unknown routes flow through your global advice like any other error.
Rule of thumb: Enable throw-exception-if-no-handler-found (and disable default resource
mappings) to route unknown-URL 404s through your @RestControllerAdvice.
They sit on a spectrum from simplest to most controllable. Pick by how much of the response you need to shape.
// 1. @ResponseStatus on the exception — status only, default body
@ResponseStatus(HttpStatus.NOT_FOUND) class NotFound extends RuntimeException {}
// 2. ResponseEntity<MyError> — full control of status + headers + custom body
@ExceptionHandler(NotFound.class)
ResponseEntity<ApiError> a(NotFound ex) {
return ResponseEntity.status(404).body(new ApiError("NOT_FOUND", ex.getMessage()));
}
// 3. ProblemDetail — standardized RFC 7807 body, minimal code
@ExceptionHandler(NotFound.class)
ProblemDetail b(NotFound ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ResponseStatus is great for trivial mappings but gives no body control. ResponseEntity
gives total control at the cost of verbosity and a bespoke shape. ProblemDetail is the sweet
spot for new APIs — standardized, little code, still extensible via setProperty.
Rule of thumb: @ResponseStatus for trivial cases, ResponseEntity when you need headers
or a fully custom body, ProblemDetail as the standardized default for new APIs.
More Web MVC interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.