Skip to content

Exception Handling Interview Questions & Answers

15 questions Updated 2026-06-26 Share:

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.

Read the in-depth guideException Handling in Spring MVC, Explained(opens in new tab)
15 of 15

@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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel