Skip to main content

Week 4 Day 4 — Exception Handling in Spring MVC

Goal

Today I want to understand exception handling in Spring MVC REST APIs.

Main questions:

  1. What happens when a controller throws an exception?
  2. Why do REST APIs need clean error responses?
  3. What is @ExceptionHandler?
  4. What is @ControllerAdvice?
  5. What is @RestControllerAdvice?
  6. What is @ResponseStatus?
  7. What is ResponseStatusException?
  8. What is ProblemDetail?
  9. What is ResponseEntityExceptionHandler?
  10. How do I handle validation errors?
  11. What are common HTTP status codes for errors?
  12. What are common exam traps?

1. Quick Review from Week 4 Day 3

In Day 3, I learned:

  • @RequestBody reads the HTTP body into a Java object.
  • @ResponseBody writes a Java return value into the HTTP response body.
  • @RestController includes @ResponseBody.
  • HttpMessageConverter converts between HTTP bodies and Java objects.
  • Jackson usually converts JSON to Java and Java to JSON.
  • DTOs define the API contract.
  • @Valid triggers request body validation.
  • ResponseEntity controls status, headers, and body.

Memory sentence:

Request body comes from the client.
Response body goes back to the client.

Today I learn what happens when something goes wrong.


2. Why Exception Handling Matters

In real APIs, many things can go wrong:

resource not found
validation error
invalid JSON
missing request parameter
wrong path variable type
database conflict
unauthorized request
forbidden action
business rule violation
unexpected server error

Without good exception handling, the API may return:

ugly stack traces
unclear messages
wrong status codes
inconsistent error formats
sensitive internal details

A good API should return:

clear HTTP status
stable error body
safe message
useful error code
validation field errors if needed
no internal stack trace

Memory sentence:

Good exception handling makes API errors predictable and safe.


3. What Happens If an Exception Is Thrown?

Example:

@GetMapping("/api/tasks/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id);
}

Service:

public TaskDto findById(Long id) {
throw new TaskNotFoundException(id);
}

If nobody handles the exception, Spring MVC will try to resolve it through its exception handling mechanism.

Possible result:

500 Internal Server Error

or another status depending on the exception type.

Problem:

The client may not get a clean, useful error response.

4. Bad Style: Try-Catch in Every Controller Method

Bad:

@GetMapping("/{id}")
public ResponseEntity<?> getTask(@PathVariable Long id) {
try {
TaskDto task = taskService.findById(id);
return ResponseEntity.ok(task);
} catch (TaskNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body("Task not found");
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Something went wrong");
}
}

Why bad?

repeated code
controllers become noisy
error response format becomes inconsistent
hard to maintain
easy to forget cases
business code mixed with error handling

Better:

Throw meaningful exceptions.
Handle them centrally.

5. Better Style: Throw Domain Exceptions

Example custom exception:

public class TaskNotFoundException extends RuntimeException {

public TaskNotFoundException(Long id) {
super("Task not found with id " + id);
}
}

Service:

@Service
public class TaskService {

public TaskDto findById(Long id) {
return taskRepository.findById(id)
.map(this::toDto)
.orElseThrow(() -> new TaskNotFoundException(id));
}
}

Controller:

@GetMapping("/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id);
}

The controller stays clean.

Exception handling happens elsewhere.

Memory sentence:

Services can throw meaningful exceptions. Controllers should stay thin.


6. @ExceptionHandler

@ExceptionHandler marks a method that handles exceptions.

Example inside one controller:

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

@GetMapping("/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id);
}

@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handleTaskNotFound(TaskNotFoundException ex) {
ErrorResponseDto error = new ErrorResponseDto(
"TASK_NOT_FOUND",
ex.getMessage()
);

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

Meaning:

If TaskNotFoundException is thrown in this controller,
handle it with handleTaskNotFound().

7. Local @ExceptionHandler

When @ExceptionHandler is inside a controller, it handles exceptions for that controller.

Example:

@RestController
public class TaskController {

@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handle(TaskNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponseDto("TASK_NOT_FOUND", ex.getMessage()));
}
}

Scope:

only TaskController

This is useful sometimes, but for REST APIs we usually prefer global handling.


8. Global Exception Handling with @ControllerAdvice

@ControllerAdvice is used for global controller-related logic.

For exceptions:

@ControllerAdvice lets exception handlers apply across many controllers.

Example:

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handleTaskNotFound(TaskNotFoundException ex) {
ErrorResponseDto error = new ErrorResponseDto(
"TASK_NOT_FOUND",
ex.getMessage()
);

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

Now the handler can apply to exceptions from multiple controllers.

Memory sentence:

@ControllerAdvice is for cross-controller exception handling.


9. @RestControllerAdvice

For REST APIs, use:

@RestControllerAdvice

Very important:

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

That means handler return values are written to the response body.

Example:

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handleTaskNotFound(TaskNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponseDto("TASK_NOT_FOUND", ex.getMessage()));
}
}

For REST APIs, this is usually cleaner than plain @ControllerAdvice.


10. Error Response DTO

A simple custom error response:

public record ErrorResponseDto(
String code,
String message
) {
}

Example response:

{
"code": "TASK_NOT_FOUND",
"message": "Task not found with id 123"
}

Better with more fields:

public record ApiErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path
) {
}

Example:

{
"timestamp": "2026-07-07T18:30:00Z",
"status": 404,
"error": "Not Found",
"code": "TASK_NOT_FOUND",
"message": "Task not found with id 123",
"path": "/api/tasks/123"
}

Memory sentence:

Error responses should have a stable structure.


11. Global Handler with Custom Error Body

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleTaskNotFound(
TaskNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
"TASK_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

This returns:

HTTP 404 Not Found

with a JSON error body.


12. Important: Do Not Leak Internal Details

Bad:

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handle(Exception ex) {
return ResponseEntity.internalServerError()
.body(ex.toString());
}

Why bad?

It may expose:

class names
SQL details
internal package names
stack traces
security information
database structure
tokens or secrets

Better:

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleUnexpected(
Exception ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
500,
"Internal Server Error",
"INTERNAL_ERROR",
"An unexpected error occurred",
request.getRequestURI()
);

return ResponseEntity.internalServerError().body(error);
}

Log the real exception internally, but do not expose it to the client.

Memory sentence:

Log internal details. Return safe messages.


13. @ResponseStatus on Exception Class

Another way to map an exception to a status:

@ResponseStatus(HttpStatus.NOT_FOUND)
public class TaskNotFoundException extends RuntimeException {

public TaskNotFoundException(Long id) {
super("Task not found with id " + id);
}
}

If this exception is thrown, Spring can return:

404 Not Found

Simple and useful.

But limitation:

Less control over custom response body.

For production REST APIs, @ControllerAdvice often gives more control.


14. @ResponseStatus on Handler Method

Example:

@PostMapping("/api/tasks")
@ResponseStatus(HttpStatus.CREATED)
public TaskDto create(@Valid @RequestBody CreateTaskRequest request) {
return taskService.create(request);
}

This is not only for exceptions.

It can also set a fixed success status.

Memory sentence:

@ResponseStatus sets a fixed HTTP status.


15. ResponseStatusException

ResponseStatusException is useful for simple cases.

Example:

@GetMapping("/api/tasks/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Task not found"
));
}

This returns:

404 Not Found

Use it for quick/simple cases.

But for larger apps, custom exceptions plus global handling are usually cleaner.


16. ResponseStatusException vs Custom Exception

TopicResponseStatusExceptionCustom Exception + Advice
Speedquickmore setup
Reuseless domain-specificreusable
Error bodyless structured unless customizedstructured
Domain meaningweakerstronger
Large appless idealbetter

Memory sentence:

ResponseStatusException is quick. Custom exceptions plus advice are cleaner for larger APIs.


17. ProblemDetail

Modern Spring supports ProblemDetail.

Simple definition:

ProblemDetail is a standard structure for HTTP API error responses.

Example:

@ExceptionHandler(TaskNotFoundException.class)
public ProblemDetail handleTaskNotFound(TaskNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
problem.setTitle("Task not found");
problem.setDetail(ex.getMessage());
problem.setProperty("code", "TASK_NOT_FOUND");
return problem;
}

Example response shape:

{
"type": "about:blank",
"title": "Task not found",
"status": 404,
"detail": "Task not found with id 123",
"instance": "/api/tasks/123",
"code": "TASK_NOT_FOUND"
}

Memory sentence:

ProblemDetail gives a standard error response format.


18. Custom DTO vs ProblemDetail

TopicCustom DTOProblemDetail
Formatfully customstandard format
Fieldsany fieldsstandard fields plus custom properties
Good forcompany-specific API stylestandard HTTP API errors
Modern Spring supportyesyes
Flexibilityhighhigh, with properties

Both are valid.

For certification, know the concept:

Spring MVC can handle exceptions with @ExceptionHandler and return ResponseEntity, custom DTOs, or ProblemDetail.

19. Validation Errors

From Day 3:

public record CreateTaskRequest(

@NotBlank
String title,

@NotBlank
String priority
) {
}

Controller:

@PostMapping("/api/tasks")
public TaskDto create(@Valid @RequestBody CreateTaskRequest request) {
return taskService.create(request);
}

Invalid request:

{
"title": "",
"priority": ""
}

Validation fails before the controller body runs.

Usually response:

400 Bad Request

But default response may not be the format I want.

So we customize it.


20. Common Validation Exception

For invalid @Valid @RequestBody, Spring MVC commonly throws:

MethodArgumentNotValidException

This contains field errors.

Example field errors:

title must not be blank
priority must not be blank

We can handle it with @ExceptionHandler.


21. Validation Error DTO

Create DTOs:

public record ValidationErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path,
List<FieldErrorDto> fieldErrors
) {
}

public record FieldErrorDto(
String field,
String message
) {
}

Example response:

{
"timestamp": "2026-07-07T18:30:00Z",
"status": 400,
"error": "Bad Request",
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"path": "/api/tasks",
"fieldErrors": [
{
"field": "title",
"message": "must not be blank"
},
{
"field": "priority",
"message": "must not be blank"
}
]
}

22. Handling Validation Errors

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationError(
MethodArgumentNotValidException ex,
HttpServletRequest request
) {
List<FieldErrorDto> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new FieldErrorDto(
error.getField(),
error.getDefaultMessage()
))
.toList();

ValidationErrorResponse response = new ValidationErrorResponse(
Instant.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
"VALIDATION_ERROR",
"Request validation failed",
request.getRequestURI(),
fieldErrors
);

return ResponseEntity.badRequest().body(response);
}
}

Memory sentence:

MethodArgumentNotValidException is important for request body validation errors.


23. Method Parameter Validation Errors

Example:

@RestController
@Validated
@RequestMapping("/api/tasks")
public class TaskController {

@GetMapping
public List<TaskDto> list(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size
) {
return taskService.findPage(page, size);
}
}

Invalid request:

GET /api/tasks?page=-1&size=500

This is not request body validation.

This is method parameter validation.

Depending on Spring version and setup, the exception type may differ.

Exam-safe memory:

@RequestBody validation commonly causes MethodArgumentNotValidException.
Request parameter validation uses method validation and may produce a constraint violation or method validation exception.

24. Missing Request Parameter

Controller:

@GetMapping("/api/tasks")
public List<TaskDto> list(@RequestParam String status) {
return taskService.findByStatus(status);
}

Request:

GET /api/tasks

Because status is required by default, Spring returns:

400 Bad Request

This is not the same as Bean Validation.

It is a Spring MVC binding/request error.


25. Type Conversion Error

Controller:

@GetMapping("/api/tasks/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id);
}

Request:

GET /api/tasks/abc

Spring cannot convert:

abc -> Long

Usually response:

400 Bad Request

Again, this is a binding/type conversion error.


26. Invalid JSON Error

Request:

POST /api/tasks
Content-Type: application/json

{
"title":
}

Spring cannot parse the JSON body.

Usually response:

400 Bad Request

Common exception category:

message not readable

In Spring MVC, this is commonly represented by:

HttpMessageNotReadableException

27. Handling Invalid JSON

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiErrorResponse> handleInvalidJson(
HttpMessageNotReadableException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
"INVALID_JSON",
"Request body is missing or invalid",
request.getRequestURI()
);

return ResponseEntity.badRequest().body(error);
}

Important:

Do not return the raw parser exception to the client.

Return a safe message.


28. Business Rule Error

Example:

public class TaskAlreadyCompletedException extends RuntimeException {

public TaskAlreadyCompletedException(Long id) {
super("Task is already completed: " + id);
}
}

This is not a validation annotation problem.

It is a business rule problem.

Possible status:

409 Conflict

Handler:

@ExceptionHandler(TaskAlreadyCompletedException.class)
public ResponseEntity<ApiErrorResponse> handleTaskAlreadyCompleted(
TaskAlreadyCompletedException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.CONFLICT.value(),
HttpStatus.CONFLICT.getReasonPhrase(),
"TASK_ALREADY_COMPLETED",
ex.getMessage(),
request.getRequestURI()
);

return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}

Memory sentence:

Use 409 Conflict when the request conflicts with current resource state.


29. Not Found Error

Exception:

public class ResourceNotFoundException extends RuntimeException {

public ResourceNotFoundException(String resource, Object id) {
super(resource + " not found with id " + id);
}
}

Handler:

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
"RESOURCE_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

Status:

404 Not Found

30. Unauthorized vs Forbidden

This is a common interview and exam trap.

401 Unauthorized

Means:

The client is not authenticated or authentication failed.

Example:

missing token
invalid token
expired token

403 Forbidden

Means:

The client is authenticated but not allowed to do this action.

Example:

normal user tries to access admin endpoint
user tries to access another tenant's data

Memory sentence:

401 = who are you? 403 = you are known, but not allowed.


31. Common Error Status Codes

SituationStatus
Validation error400 Bad Request
Invalid JSON400 Bad Request
Missing request parameter400 Bad Request
Path variable conversion error400 Bad Request
Authentication missing or invalid401 Unauthorized
Authenticated but not allowed403 Forbidden
Resource not found404 Not Found
HTTP method wrong405 Method Not Allowed
Business conflict409 Conflict
Unsupported request body type415 Unsupported Media Type
Unexpected server error500 Internal Server Error

32. ResponseEntityExceptionHandler

Spring provides ResponseEntityExceptionHandler.

Simple definition:

ResponseEntityExceptionHandler is a convenient base class for handling many built-in Spring MVC exceptions.

Example:

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleTaskNotFound(
TaskNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
"TASK_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

This can be useful when you want to customize Spring MVC’s built-in exception handling.

For the exam:

Know it exists.
Know it is used with @ControllerAdvice / @RestControllerAdvice.
Know it helps handle built-in MVC exceptions.

33. Exception Handler Resolution

When an exception happens, Spring searches for a matching handler.

Simplified order:

1. Look for @ExceptionHandler in the controller.
2. Look for @ExceptionHandler in @ControllerAdvice / @RestControllerAdvice.
3. Use built-in Spring MVC exception handling.
4. If unresolved, return generic server error.

Important:

A more specific exception handler is preferred over a generic one.

Example:

@ExceptionHandler(TaskNotFoundException.class)

is more specific than:

@ExceptionHandler(Exception.class)

34. Generic Exception Handler

A generic handler is useful as a final fallback.

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleUnexpected(
Exception ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
"INTERNAL_ERROR",
"An unexpected error occurred",
request.getRequestURI()
);

return ResponseEntity.internalServerError().body(error);
}

Important:

Do not swallow all errors silently.
Log the exception.
Return safe response.

Example with logging:

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleUnexpected(
Exception ex,
HttpServletRequest request
) {
log.error("Unexpected error", ex);

ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
500,
"Internal Server Error",
"INTERNAL_ERROR",
"An unexpected error occurred",
request.getRequestURI()
);

return ResponseEntity.internalServerError().body(error);
}

35. Full Global Exception Handler Example

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger log =
LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
"RESOURCE_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

@ExceptionHandler(TaskAlreadyCompletedException.class)
public ResponseEntity<ApiErrorResponse> handleConflict(
TaskAlreadyCompletedException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.CONFLICT.value(),
HttpStatus.CONFLICT.getReasonPhrase(),
"TASK_ALREADY_COMPLETED",
ex.getMessage(),
request.getRequestURI()
);

return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidation(
MethodArgumentNotValidException ex,
HttpServletRequest request
) {
List<FieldErrorDto> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new FieldErrorDto(
error.getField(),
error.getDefaultMessage()
))
.toList();

ValidationErrorResponse response = new ValidationErrorResponse(
Instant.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
"VALIDATION_ERROR",
"Request validation failed",
request.getRequestURI(),
fieldErrors
);

return ResponseEntity.badRequest().body(response);
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiErrorResponse> handleInvalidJson(
HttpMessageNotReadableException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
"INVALID_JSON",
"Request body is missing or invalid",
request.getRequestURI()
);

return ResponseEntity.badRequest().body(error);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleUnexpected(
Exception ex,
HttpServletRequest request
) {
log.error("Unexpected error", ex);

ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
"INTERNAL_ERROR",
"An unexpected error occurred",
request.getRequestURI()
);

return ResponseEntity.internalServerError().body(error);
}
}

DTOs:

public record ApiErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path
) {
}

public record ValidationErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path,
List<FieldErrorDto> fieldErrors
) {
}

public record FieldErrorDto(
String field,
String message
) {
}

36. Exception Handling Flow

Example request:

GET /api/tasks/999

Flow:

1. Request reaches TaskController.
2. Controller calls TaskService.
3. TaskService cannot find task.
4. TaskService throws ResourceNotFoundException.
5. DispatcherServlet catches the exception.
6. Spring looks for an @ExceptionHandler.
7. GlobalExceptionHandler.handleNotFound() handles it.
8. Error DTO is returned.
9. Jackson converts error DTO to JSON.
10. Client receives 404 response.

Memory sentence:

Exception -> Handler -> Error DTO -> JSON response.


37. Clean Controller with Global Exception Handling

Controller:

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

private final TaskService taskService;

public TaskController(TaskService taskService) {
this.taskService = taskService;
}

@GetMapping("/{id}")
public TaskDto get(@PathVariable Long id) {
return taskService.findById(id);
}

@PostMapping
public ResponseEntity<TaskDto> create(
@Valid @RequestBody CreateTaskRequest request
) {
TaskDto created = taskService.create(request);
URI location = URI.create("/api/tasks/" + created.id());

return ResponseEntity.created(location).body(created);
}
}

No try-catch.

Why?

Known exceptions are handled globally.
Validation errors are handled globally.
Unexpected errors are handled safely.

This is clean REST API design.


38. Good Error Response Design

A good error response should be:

consistent
safe
machine-readable
human-readable
easy to debug
not too verbose
not leaking internals

Useful fields:

timestamp
status
error
code
message
path
fieldErrors
traceId

Example:

{
"timestamp": "2026-07-07T18:30:00Z",
"status": 404,
"error": "Not Found",
"code": "TASK_NOT_FOUND",
"message": "Task not found with id 123",
"path": "/api/tasks/123"
}

39. Trace ID in Error Responses

In production, adding a trace ID is useful.

Example:

{
"status": 500,
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"traceId": "abc-123"
}

Why useful?

Client reports traceId.
Backend team searches logs by traceId.
Problem is easier to debug.

This is especially useful in distributed systems.


40. What Not to Put in Error Responses

Avoid exposing:

stack traces
SQL queries
database table names
internal class names
file paths
tokens
passwords
personal data
secret keys
full exception dumps

Return safe messages.

Log detailed errors internally.

Memory sentence:

Error responses are for clients. Logs are for developers.


41. Common Exam Traps

Trap 1

@ExceptionHandler handles exceptions thrown by controller methods.


Trap 2

A local @ExceptionHandler inside a controller applies to that controller.


Trap 3

@ControllerAdvice applies globally across controllers.


Trap 4

@RestControllerAdvice equals @ControllerAdvice plus @ResponseBody.


Trap 5

@ResponseStatus sets a fixed HTTP status but gives less control over the response body.


Trap 6

ResponseStatusException is useful for quick/simple HTTP errors.


Trap 7

MethodArgumentNotValidException is commonly used for failed @Valid @RequestBody.


Trap 8

Invalid JSON usually means 400 Bad Request.


Trap 9

Business conflicts often map to 409 Conflict.


Trap 10

Do not expose stack traces or internal exception details to API clients.


42. Real Exam Question: @ExceptionHandler

Question:

What does @ExceptionHandler do?

Answer:

@ExceptionHandler marks a method that handles specific exception types thrown from controller methods.


43. Real Exam Question: Local vs Global Handler

Question:

What is the difference between an @ExceptionHandler inside a controller and one inside @ControllerAdvice?

Answer:

An @ExceptionHandler inside a controller handles exceptions for that controller. An @ExceptionHandler inside @ControllerAdvice can apply globally across multiple controllers.


44. Real Exam Question: @ControllerAdvice

Question:

What is @ControllerAdvice used for?

Answer:

@ControllerAdvice is used for cross-controller concerns, especially global exception handling in Spring MVC.


45. Real Exam Question: @RestControllerAdvice

Question:

What is @RestControllerAdvice?

Answer:

@RestControllerAdvice is a convenience annotation that combines @ControllerAdvice and @ResponseBody, so exception handler return values are written directly to the response body.


46. Real Exam Question: @ResponseStatus

Question:

What does @ResponseStatus do?

Answer:

@ResponseStatus sets a fixed HTTP status for a controller method or exception class.


47. Real Exam Question: ResponseStatusException

Question:

What is ResponseStatusException used for?

Answer:

ResponseStatusException is used to throw an exception with a specific HTTP status and reason, often for simple cases such as returning 404 Not Found.


48. Real Exam Question: Validation Error

Question:

Which exception commonly occurs when @Valid @RequestBody validation fails in Spring MVC?

Answer:

MethodArgumentNotValidException.


49. Real Exam Question: Invalid JSON

Question:

What status should invalid JSON usually return?

Answer:

400 Bad Request.


50. Real Exam Question: Not Found

Question:

What status should a missing resource usually return?

Answer:

404 Not Found.


51. Real Exam Question: Conflict

Question:

What status is commonly used for a business conflict?

Answer:

409 Conflict.


52. Real Exam Question: 401 vs 403

Question:

What is the difference between 401 Unauthorized and 403 Forbidden?

Answer:

401 Unauthorized means authentication is missing or invalid. 403 Forbidden means the user is authenticated but not allowed to perform the action.


53. Real Exam Question: ProblemDetail

Question:

What is ProblemDetail?

Answer:

ProblemDetail is a standard structure for HTTP API error responses. It contains fields such as status, title, detail, type, and instance, and can also include custom properties.


54. Real Exam Question: ResponseEntityExceptionHandler

Question:

What is ResponseEntityExceptionHandler?

Answer:

ResponseEntityExceptionHandler is a convenient base class for handling many built-in Spring MVC exceptions and returning structured error responses.


55. Interview Answer

Question:

How do you handle exceptions in a Spring Boot REST API?

Good answer:

I usually create custom domain exceptions, such as ResourceNotFoundException, and handle them globally with @RestControllerAdvice. Inside that advice class, I use @ExceptionHandler methods to map exceptions to HTTP status codes and structured error responses. For example, a not found exception maps to 404, validation errors map to 400, and business conflicts map to 409. I also add a generic fallback handler for unexpected exceptions and log the internal details without exposing them to the client.


56. Interview Answer

Question:

What is the difference between @ExceptionHandler and @ControllerAdvice?

Good answer:

@ExceptionHandler marks a method that handles a specific exception type. If it is placed inside a controller, it handles exceptions only for that controller. @ControllerAdvice allows exception handling methods to apply across multiple controllers. For REST APIs, I usually use @RestControllerAdvice, which combines @ControllerAdvice with @ResponseBody.


57. Interview Answer

Question:

How do you handle validation errors?

Good answer:

For request body validation, I add Bean Validation annotations to the request DTO and use @Valid @RequestBody in the controller. If validation fails, Spring MVC commonly throws MethodArgumentNotValidException. I handle that exception in a global @RestControllerAdvice and return a structured 400 Bad Request response with field-level errors, such as the field name and validation message.


58. Interview Answer

Question:

Why should you not return raw exception messages to clients?

Good answer:

Raw exception messages can expose internal implementation details, such as class names, SQL queries, table names, file paths, stack traces, or even sensitive data. A production API should log detailed errors internally but return safe, stable, client-friendly error messages with appropriate status codes and error codes.


59. Interview Answer

Question:

When would you use ResponseStatusException?

Good answer:

I use ResponseStatusException for simple cases where I quickly want to return a specific HTTP status, such as 404 Not Found. For larger applications, I prefer custom exceptions plus a global @RestControllerAdvice, because that gives a consistent error response format and better domain meaning.


60. Tiny Code Practice

Create an exception:

public class ResourceNotFoundException extends RuntimeException {

public ResourceNotFoundException(String resource, Object id) {
super(resource + " not found with id " + id);
}
}

Create error DTO:

public record ApiErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path
) {
}

Create global handler:

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
404,
"Not Found",
"RESOURCE_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

Questions:

  1. What annotation makes this handler global?
  2. What exception does it handle?
  3. What HTTP status does it return?
  4. Why use HttpServletRequest?
  5. Why use an error DTO?

Answers:

  1. @RestControllerAdvice
  2. ResourceNotFoundException
  3. 404 Not Found
  4. To read the request path.
  5. To return a consistent JSON error response.

61. Tiny Bug Practice 1

Problem:

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handle(Exception ex) {
return ResponseEntity.internalServerError().body(ex.toString());
}

Question:

What is wrong?

Answer:

It returns raw exception details to the client. This may expose internal class names, stack traces, SQL details, or sensitive information. Log the exception internally and return a safe generic message.


62. Tiny Bug Practice 2

Problem:

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public ApiErrorResponse handle(ResourceNotFoundException ex) {
return new ApiErrorResponse(...);
}
}

The REST API does not return JSON as expected.

Question:

What might be missing?

Answer:

Plain @ControllerAdvice does not automatically include @ResponseBody. For REST APIs, use @RestControllerAdvice or add @ResponseBody to the handler method.


63. Tiny Bug Practice 3

Problem:

@PostMapping("/api/tasks")
public TaskDto create(@RequestBody CreateTaskRequest request) {
return taskService.create(request);
}

DTO:

public record CreateTaskRequest(
@NotBlank String title
) {
}

Validation does not run.

Question:

What is missing?

Answer:

@Valid is missing.

Fix:

@PostMapping("/api/tasks")
public TaskDto create(@Valid @RequestBody CreateTaskRequest request) {
return taskService.create(request);
}

Practice Questions and Answers

Question 1

Why do REST APIs need good exception handling?

Answer:

REST APIs need good exception handling to return clear status codes, consistent error bodies, safe messages, and useful error codes without leaking internal details.


Question 2

What does @ExceptionHandler do?

Answer:

@ExceptionHandler marks a method that handles specific exception types thrown by controller methods.


Question 3

What is a local exception handler?

Answer:

A local exception handler is an @ExceptionHandler method inside a controller. It handles exceptions for that controller.


Question 4

What does @ControllerAdvice do?

Answer:

@ControllerAdvice provides cross-controller behavior, commonly global exception handling.


Question 5

What does @RestControllerAdvice include?

Answer:

@RestControllerAdvice includes @ControllerAdvice and @ResponseBody.


Question 6

What is a good structure for an API error response?

Answer:

A good API error response can include timestamp, status, error, code, message, path, and field errors if validation fails.


Question 7

Why should I not expose stack traces to API clients?

Answer:

Stack traces can expose internal class names, SQL details, file paths, package names, and sensitive implementation details.


Question 8

What does @ResponseStatus do?

Answer:

@ResponseStatus sets a fixed HTTP status for a controller method or exception class.


Question 9

What is ResponseStatusException?

Answer:

ResponseStatusException is an exception that carries an HTTP status and optional reason, useful for simple HTTP errors.


Question 10

What is ProblemDetail?

Answer:

ProblemDetail is a standard structure for HTTP API error responses, with fields like status, title, detail, type, and instance.


Question 11

How do I handle validation errors from @Valid @RequestBody?

Answer:

Handle MethodArgumentNotValidException in a global @RestControllerAdvice and return a structured 400 Bad Request response with field errors.


Question 12

What exception commonly represents request body validation failure?

Answer:

MethodArgumentNotValidException.


Question 13

What status should invalid JSON return?

Answer:

Invalid JSON should usually return 400 Bad Request.


Question 14

What status should missing resources return?

Answer:

Missing resources should usually return 404 Not Found.


Question 15

What status can business conflicts return?

Answer:

Business conflicts can return 409 Conflict.


Question 16

What is the difference between 401 and 403?

Answer:

401 Unauthorized means authentication is missing or invalid. 403 Forbidden means the user is authenticated but not allowed.


Question 17

What is ResponseEntityExceptionHandler?

Answer:

ResponseEntityExceptionHandler is a convenient base class for handling many built-in Spring MVC exceptions.


Question 18

Why is a generic Exception handler useful?

Answer:

A generic exception handler is useful as a final fallback for unexpected errors.


Question 19

What should a generic exception handler avoid?

Answer:

It should avoid returning raw exception details, stack traces, SQL details, internal class names, or sensitive data to the client.


Question 20

Why is global exception handling better than try-catch in every controller method?

Answer:

Global exception handling avoids repeated try-catch blocks, keeps controllers clean, and gives the API a consistent error response format.

Final Memory Sentences

  • @ExceptionHandler handles specific exceptions.
  • Local exception handlers apply to one controller.
  • @ControllerAdvice applies across controllers.
  • @RestControllerAdvice equals @ControllerAdvice plus @ResponseBody.
  • Use global exception handling for consistent REST API errors.
  • @ResponseStatus sets a fixed HTTP status.
  • ResponseStatusException is useful for simple HTTP errors.
  • ProblemDetail is a standard error response structure.
  • MethodArgumentNotValidException commonly represents failed @Valid @RequestBody.
  • Invalid JSON usually returns 400.
  • Missing resources usually return 404.
  • Business conflicts often return 409.
  • 401 means not authenticated.
  • 403 means authenticated but not allowed.
  • ResponseEntityExceptionHandler helps handle built-in MVC exceptions.
  • Do not expose stack traces or internal details to clients.
  • Log detailed errors internally.
  • Return safe, stable, structured error responses.