Zum Hauptinhalt springen

Woche 4, Tag 4 — Exception Handling in Spring MVC

Ziel

Heute verstehst du Exception Handling in Spring MVC REST APIs.

Die Kernfragen:

  1. Was passiert, wenn ein Controller eine Exception wirft?
  2. Warum brauchen REST APIs saubere Fehlerantworten?
  3. Was ist @ExceptionHandler?
  4. Was ist @ControllerAdvice?
  5. Was ist @RestControllerAdvice?
  6. Was ist @ResponseStatus?
  7. Was ist ResponseStatusException?
  8. Was ist ProblemDetail?
  9. Was ist ResponseEntityExceptionHandler?
  10. Wie behandelst du Validierungsfehler?
  11. Welche HTTP-Statuscodes sind bei Fehlern üblich?
  12. Welche typischen Prüfungsfallen gibt es?

1. Kurz-Wiederholung aus Woche 4, Tag 3

An Tag 3 hast du gelernt:

  • @RequestBody liest den HTTP-Body in ein Java-Objekt.
  • @ResponseBody schreibt einen Java-Rückgabewert in den HTTP-Response-Body.
  • @RestController enthält @ResponseBody.
  • HttpMessageConverter konvertiert zwischen HTTP-Bodies und Java-Objekten.
  • Jackson konvertiert üblicherweise JSON zu Java und Java zu JSON.
  • DTOs definieren den API-Vertrag.
  • @Valid löst die Validierung des Request-Bodys aus.
  • ResponseEntity steuert Status, Header und Body.

Merksatz:


Request body kommt vom Client.
Response body geht zurück an den Client.

Heute lernst du, was passiert, wenn etwas schiefgeht.


2. Warum Exception Handling wichtig ist

In echten APIs kann vieles schiefgehen:


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

Ohne gutes Exception Handling kann die API zurückgeben:


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

Eine gute API sollte zurückgeben:


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

Merksatz:

Gutes Exception Handling macht API-Fehler vorhersehbar und sicher.


3. Was passiert, wenn eine Exception geworfen wird?

Beispiel:


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

Service:


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

Wenn niemand die Exception behandelt, versucht Spring MVC, sie über seinen Exception-Handling-Mechanismus aufzulösen.

Mögliches Ergebnis:


500 Internal Server Error

oder ein anderer Status — je nach Exception-Typ.

Problem:


Der Client bekommt möglicherweise keine saubere, hilfreiche Fehlerantwort.

4. Schlechter Stil: Try-Catch in jeder Controller-Methode

Schlecht:


@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");
}
}

Warum schlecht?


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

Besser:


Throw meaningful exceptions.
Handle them centrally.

5. Besserer Stil: Domain Exceptions werfen

Beispiel für eine eigene 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);
}

Der Controller bleibt sauber.

Exception Handling passiert woanders.

Merksatz:

Services können sinnvolle Exceptions werfen. Controller sollten dünn bleiben.


6. @ExceptionHandler

@ExceptionHandler markiert eine Methode, die Exceptions behandelt.

Beispiel innerhalb eines Controllers:


@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);
}
}

Bedeutung:


Wenn in diesem Controller eine TaskNotFoundException geworfen wird,
behandle sie mit handleTaskNotFound().

7. Lokaler @ExceptionHandler

Wenn @ExceptionHandler in einem Controller liegt, behandelt er Exceptions für diesen Controller.

Beispiel:


@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()));
}
}

Geltungsbereich:


nur TaskController

Das ist manchmal nützlich — für REST APIs bevorzugen wir aber meist globales Handling.


8. Globales Exception Handling mit @ControllerAdvice

@ControllerAdvice wird für globale controller-bezogene Logik genutzt.

Für Exceptions:

@ControllerAdvice lässt Exception Handler über viele Controller hinweg gelten.

Beispiel:


@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);
}
}

Jetzt kann der Handler Exceptions aus mehreren Controllern behandeln.

Merksatz:

@ControllerAdvice ist für controller-übergreifendes Exception Handling.


9. @RestControllerAdvice

Für REST APIs nutze:


@RestControllerAdvice

Sehr wichtig:


@RestControllerAdvice = @ControllerAdvice + @ResponseBody

Das heißt: Rückgabewerte des Handlers werden in den Response-Body geschrieben.

Beispiel:


@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()));
}
}

Für REST APIs ist das meist sauberer als einfaches @ControllerAdvice.


10. Error-Response-DTO

Eine einfache eigene Fehlerantwort:


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

Beispielantwort:


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

Besser mit mehr Feldern:


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

Beispiel:


{
"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"
}

Merksatz:

Fehlerantworten sollten eine stabile Struktur haben.


11. Globaler Handler mit eigenem 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);
}
}

Das liefert:


HTTP 404 Not Found

mit einem JSON-Error-Body.


12. Wichtig: Keine internen Details preisgeben

Schlecht:


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

Warum schlecht?

Das kann preisgeben:


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

Besser:


@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);
}

Logge die echte Exception intern — gib sie aber nicht an den Client weiter.

Merksatz:

Interne Details loggen. Sichere Meldungen zurückgeben.


13. @ResponseStatus auf der Exception-Klasse

Ein anderer Weg, eine Exception einem Status zuzuordnen:


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

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

Wenn diese Exception geworfen wird, kann Spring zurückgeben:


404 Not Found

Einfach und nützlich.

Aber Einschränkung:


Weniger Kontrolle über den eigenen Response-Body.

Für produktive REST APIs gibt @ControllerAdvice oft mehr Kontrolle.


14. @ResponseStatus auf der Handler-Methode

Beispiel:


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

Das gilt nicht nur für Exceptions.

Es kann auch einen festen Erfolgs-Status setzen.

Merksatz:

@ResponseStatus setzt einen festen HTTP-Status.


15. ResponseStatusException

ResponseStatusException ist nützlich für einfache Fälle.

Beispiel:


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

Das liefert:


404 Not Found

Nutze es für schnelle/einfache Fälle.

Für größere Apps sind eigene Exceptions plus globales Handling meist sauberer.


16. ResponseStatusException vs. eigene Exception

ThemaResponseStatusExceptionEigene Exception + Advice
Geschwindigkeitschnellmehr Setup
Wiederverwendungweniger domain-spezifischwiederverwendbar
Error bodyweniger strukturiert ohne Anpassungstrukturiert
Domain-Bedeutungschwächerstärker
Große Appweniger idealbesser

Merksatz:

ResponseStatusException ist schnell. Eigene Exceptions plus Advice sind für größere APIs sauberer.


17. ProblemDetail

Modernes Spring unterstützt ProblemDetail.

Einfache Definition:

ProblemDetail ist eine Standardstruktur für HTTP-API-Fehlerantworten.

Beispiel:


@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;
}

Beispiel für die Antwortstruktur:


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

Merksatz:

ProblemDetail liefert ein standardisiertes Fehlerantwort-Format.


18. Eigenes DTO vs. ProblemDetail

ThemaEigenes DTOProblemDetail
Formatvollständig eigenStandardformat
Felderbeliebige FelderStandardfelder plus eigene Properties
Gut fürfirmenspezifischer API-Stilstandardisierte HTTP-API-Fehler
Modernes Springjaja
Flexibilitäthochhoch, mit Properties

Beides ist gültig.

Für die Zertifizierung kenne das Konzept:


Spring MVC kann Exceptions mit @ExceptionHandler behandeln und ResponseEntity, eigene DTOs oder ProblemDetail zurückgeben.

19. Validierungsfehler

Aus Tag 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);
}

Ungültiger Request:


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

Die Validierung schlägt fehl, bevor der Controller-Body ausgeführt wird.

Übliche Antwort:


400 Bad Request

Aber die Standardantwort hat vielleicht nicht das Format, das du willst.

Also passen wir sie an.


20. Häufige Validierungs-Exception

Bei ungültigem @Valid @RequestBody wirft Spring MVC üblicherweise:


MethodArgumentNotValidException

Darin stecken Feldfehler.

Beispiel-Feldfehler:


title must not be blank
priority must not be blank

Du kannst das mit @ExceptionHandler behandeln.


21. Validierungsfehler-DTO

Erstelle 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
) {
}

Beispielantwort:


{
"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. Validierungsfehler behandeln


@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);
}
}

Merksatz:

MethodArgumentNotValidException ist wichtig für Request-Body-Validierungsfehler.


23. Validierungsfehler bei Methodenparametern

Beispiel:


@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);
}
}

Ungültiger Request:


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

Das ist keine Request-Body-Validierung.

Das ist Methodenparameter-Validierung.

Je nach Spring-Version und Setup kann der Exception-Typ unterschiedlich sein.

Prüfungssicherer Merksatz:


@RequestBody-Validierung löst üblicherweise MethodArgumentNotValidException aus.
Request-Parameter-Validierung nutzt Method Validation und kann eine Constraint Violation oder Method Validation Exception erzeugen.

24. Fehlender Request-Parameter

Controller:


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

Request:


GET /api/tasks

Weil status standardmäßig erforderlich ist, liefert Spring:


400 Bad Request

Das ist nicht dasselbe wie Bean Validation.

Das ist ein Spring-MVC-Binding-/Request-Fehler.


25. Typkonvertierungsfehler

Controller:


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

Request:


GET /api/tasks/abc

Spring kann nicht konvertieren:


abc -> Long

Übliche Antwort:


400 Bad Request

Auch das ist ein Binding-/Typkonvertierungsfehler.


26. Ungültiger JSON-Fehler

Request:


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

{
"title":
}

Spring kann den JSON-Body nicht parsen.

Übliche Antwort:


400 Bad Request

Häufige Exception-Kategorie:


message not readable

In Spring MVC wird das üblicherweise repräsentiert durch:


HttpMessageNotReadableException

27. Ungültiges JSON behandeln


@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);
}

Wichtig:


Gib die rohe Parser-Exception nicht an den Client zurück.

Gib eine sichere Meldung zurück.


28. Business-Rule-Fehler

Beispiel:


public class TaskAlreadyCompletedException extends RuntimeException {

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

Das ist kein Validierungs-Annotations-Problem.

Das ist ein Business-Rule-Problem.

Möglicher 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);
}

Merksatz:

Nutze 409 Conflict, wenn der Request mit dem aktuellen Ressourcen-Zustand kollidiert.


29. Not-Found-Fehler

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

Das ist eine häufige Interview- und Prüfungsfalle.

401 Unauthorized

Bedeutet:


Der Client ist nicht authentifiziert oder die Authentifizierung ist fehlgeschlagen.

Beispiel:


fehlender Token
ungültiger Token
abgelaufener Token

403 Forbidden

Bedeutet:


Der Client ist authentifiziert, darf diese Aktion aber nicht ausführen.

Beispiel:


normaler Nutzer versucht Admin-Endpoint aufzurufen
Nutzer versucht auf Daten eines anderen Tenants zuzugreifen

Merksatz:

401 = Wer bist du? 403 = Du bist bekannt, aber nicht berechtigt.


31. Häufige Fehler-Statuscodes

SituationStatus
Validierungsfehler400 Bad Request
Ungültiges JSON400 Bad Request
Fehlender Request-Parameter400 Bad Request
Path-Variable-Konvertierungsfehler400 Bad Request
Authentifizierung fehlt oder ungültig401 Unauthorized
Authentifiziert, aber nicht berechtigt403 Forbidden
Ressource nicht gefunden404 Not Found
Falsche HTTP-Methode405 Method Not Allowed
Business-Konflikt409 Conflict
Nicht unterstützter Request-Body-Typ415 Unsupported Media Type
Unerwarteter Serverfehler500 Internal Server Error

32. ResponseEntityExceptionHandler

Spring stellt ResponseEntityExceptionHandler bereit.

Einfache Definition:

ResponseEntityExceptionHandler ist eine praktische Basisklasse für viele eingebaute Spring-MVC-Exceptions.

Beispiel:


@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);
}
}

Das ist nützlich, wenn du das eingebaute Exception Handling von Spring MVC anpassen willst.

Für die Prüfung:


Kenne, dass es existiert.
Kenne, dass es mit @ControllerAdvice / @RestControllerAdvice genutzt wird.
Kenne, dass es beim Behandeln eingebauter MVC-Exceptions hilft.

33. Auflösung von Exception Handlern

Wenn eine Exception auftritt, sucht Spring nach einem passenden Handler.

Vereinfachte Reihenfolge:


1. Suche @ExceptionHandler im Controller.
2. Suche @ExceptionHandler in @ControllerAdvice / @RestControllerAdvice.
3. Nutze eingebautes Spring-MVC-Exception-Handling.
4. Wenn unaufgelöst: generischen Serverfehler zurückgeben.

Wichtig:


Ein spezifischerer Exception Handler hat Vorrang vor einem generischen.

Beispiel:


@ExceptionHandler(TaskNotFoundException.class)

ist spezifischer als:


@ExceptionHandler(Exception.class)

34. Generischer Exception Handler

Ein generischer Handler ist als letzter Fallback nützlich.


@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);
}

Wichtig:


Verschlucke nicht alle Fehler still.
Logge die Exception.
Gib eine sichere Antwort zurück.

Beispiel mit 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. Vollständiges Beispiel für globalen Exception Handler


@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. Ablauf des Exception Handlings

Beispiel-Request:


GET /api/tasks/999

Ablauf:


1. Request erreicht TaskController.
2. Controller ruft TaskService auf.
3. TaskService findet den Task nicht.
4. TaskService wirft ResourceNotFoundException.
5. DispatcherServlet fängt die Exception.
6. Spring sucht einen @ExceptionHandler.
7. GlobalExceptionHandler.handleNotFound() behandelt sie.
8. Error-DTO wird zurückgegeben.
9. Jackson konvertiert Error-DTO zu JSON.
10. Client erhält 404-Antwort.

Merksatz:

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


37. Sauberer Controller mit globalem 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);
}
}

Kein try-catch.

Warum?


Bekannte Exceptions werden global behandelt.
Validierungsfehler werden global behandelt.
Unerwartete Fehler werden sicher behandelt.

Das ist sauberes REST-API-Design.


38. Gutes Error-Response-Design

Eine gute Fehlerantwort sollte sein:


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

Nützliche Felder:


timestamp
status
error
code
message
path
fieldErrors
traceId

Beispiel:


{
"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 Fehlerantworten

In Produktion ist eine Trace ID nützlich.

Beispiel:


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

Warum nützlich?


Client meldet traceId.
Backend-Team sucht Logs per traceId.
Problem ist leichter zu debuggen.

Das ist besonders in verteilten Systemen nützlich.


40. Was nicht in Fehlerantworten gehört

Vermeide, preiszugeben:


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

Gib sichere Meldungen zurück.

Logge detaillierte Fehler intern.

Merksatz:

Fehlerantworten sind für Clients. Logs sind für Entwickler.


41. Typische Prüfungsfallen

Falle 1

@ExceptionHandler behandelt Exceptions, die von Controller-Methoden geworfen werden.


Falle 2

Ein lokaler @ExceptionHandler in einem Controller gilt für diesen Controller.


Falle 3

@ControllerAdvice gilt global über Controller hinweg.


Falle 4

@RestControllerAdvice entspricht @ControllerAdvice plus @ResponseBody.


Falle 5

@ResponseStatus setzt einen festen HTTP-Status, gibt aber weniger Kontrolle über den Response-Body.


Falle 6

ResponseStatusException ist nützlich für schnelle/einfache HTTP-Fehler.


Falle 7

MethodArgumentNotValidException wird üblicherweise bei fehlgeschlagenem @Valid @RequestBody genutzt.


Falle 8

Ungültiges JSON bedeutet üblicherweise 400 Bad Request.


Falle 9

Business-Konflikte werden oft 409 Conflict zugeordnet.


Falle 10

Gib keine Stack Traces oder internen Exception-Details an API-Clients weiter.


42. Echte Prüfungsfrage: @ExceptionHandler

Frage:

Was macht @ExceptionHandler?

Antwort:

@ExceptionHandler markiert eine Methode, die bestimmte Exception-Typen behandelt, die von Controller-Methoden geworfen werden.


43. Echte Prüfungsfrage: Lokaler vs. globaler Handler

Frage:

Was ist der Unterschied zwischen einem @ExceptionHandler in einem Controller und einem in @ControllerAdvice?

Antwort:

Ein @ExceptionHandler in einem Controller behandelt Exceptions für diesen Controller. Ein @ExceptionHandler in @ControllerAdvice kann global über mehrere Controller hinweg gelten.


44. Echte Prüfungsfrage: @ControllerAdvice

Frage:

Wofür wird @ControllerAdvice genutzt?

Antwort:

@ControllerAdvice wird für controller-übergreifende Belange genutzt — besonders globales Exception Handling in Spring MVC.


45. Echte Prüfungsfrage: @RestControllerAdvice

Frage:

Was ist @RestControllerAdvice?

Antwort:

@RestControllerAdvice ist eine Convenience-Annotation, die @ControllerAdvice und @ResponseBody kombiniert — Rückgabewerte von Exception Handlern landen direkt im Response-Body.


46. Echte Prüfungsfrage: @ResponseStatus

Frage:

Was macht @ResponseStatus?

Antwort:

@ResponseStatus setzt einen festen HTTP-Status für eine Controller-Methode oder Exception-Klasse.


47. Echte Prüfungsfrage: ResponseStatusException

Frage:

Wofür wird ResponseStatusException genutzt?

Antwort:

ResponseStatusException wirft eine Exception mit einem bestimmten HTTP-Status und Grund — oft für einfache Fälle wie 404 Not Found.


48. Echte Prüfungsfrage: Validierungsfehler

Frage:

Welche Exception tritt üblicherweise auf, wenn @Valid @RequestBody-Validierung in Spring MVC fehlschlägt?

Antwort:

MethodArgumentNotValidException.


49. Echte Prüfungsfrage: Ungültiges JSON

Frage:

Welchen Status sollte ungültiges JSON üblicherweise zurückgeben?

Antwort:

400 Bad Request.


50. Echte Prüfungsfrage: Not Found

Frage:

Welchen Status sollte eine fehlende Ressource üblicherweise zurückgeben?

Antwort:

404 Not Found.


51. Echte Prüfungsfrage: Conflict

Frage:

Welcher Status wird üblicherweise für einen Business-Konflikt genutzt?

Antwort:

409 Conflict.


52. Echte Prüfungsfrage: 401 vs. 403

Frage:

Was ist der Unterschied zwischen 401 Unauthorized und 403 Forbidden?

Antwort:

401 Unauthorized bedeutet: Authentifizierung fehlt oder ist ungültig. 403 Forbidden bedeutet: Der Nutzer ist authentifiziert, darf die Aktion aber nicht ausführen.


53. Echte Prüfungsfrage: ProblemDetail

Frage:

Was ist ProblemDetail?

Antwort:

ProblemDetail ist eine Standardstruktur für HTTP-API-Fehlerantworten. Sie enthält Felder wie status, title, detail, type und instance — plus eigene Properties.


54. Echte Prüfungsfrage: ResponseEntityExceptionHandler

Frage:

Was ist ResponseEntityExceptionHandler?

Antwort:

ResponseEntityExceptionHandler ist eine praktische Basisklasse für viele eingebaute Spring-MVC-Exceptions und strukturierte Fehlerantworten.


55. Interview-Antwort

Frage:

Wie behandelst du Exceptions in einer Spring Boot REST API?

Gute Antwort:

Ich erstelle üblicherweise eigene Domain Exceptions wie ResourceNotFoundException und behandle sie global mit @RestControllerAdvice. In dieser Advice-Klasse nutze ich @ExceptionHandler-Methoden, um Exceptions HTTP-Statuscodes und strukturierte Fehlerantworten zuzuordnen. Not Found wird z. B. 404, Validierungsfehler 400, Business-Konflikte 409. Außerdem gibt es einen generischen Fallback-Handler für unerwartete Exceptions — interne Details werden geloggt, nicht an den Client gegeben.


56. Interview-Antwort

Frage:

Was ist der Unterschied zwischen @ExceptionHandler und @ControllerAdvice?

Gute Antwort:

@ExceptionHandler markiert eine Methode für einen bestimmten Exception-Typ. Im Controller gilt sie nur für diesen Controller. @ControllerAdvice lässt Exception-Handling-Methoden über mehrere Controller hinweg gelten. Für REST APIs nutze ich meist @RestControllerAdvice — das kombiniert @ControllerAdvice mit @ResponseBody.


57. Interview-Antwort

Frage:

Wie behandelst du Validierungsfehler?

Gute Antwort:

Für Request-Body-Validierung setze ich Bean-Validation-Annotations auf das Request-DTO und @Valid @RequestBody im Controller. Schlägt die Validierung fehl, wirft Spring MVC üblicherweise MethodArgumentNotValidException. Die behandle ich in einem globalen @RestControllerAdvice und liefere strukturiert 400 Bad Request mit Feldfehlern — Feldname und Validierungsmeldung.


58. Interview-Antwort

Frage:

Warum solltest du keine rohen Exception-Meldungen an Clients zurückgeben?

Gute Antwort:

Rohe Exception-Meldungen können interne Details preisgeben: Klassennamen, SQL-Queries, Tabellennamen, Dateipfade, Stack Traces oder sensible Daten. Eine produktive API loggt Fehler intern detailliert — gibt aber sichere, stabile, client-freundliche Meldungen mit passenden Status- und Error-Codes zurück.


59. Interview-Antwort

Frage:

Wann würdest du ResponseStatusException nutzen?

Gute Antwort:

Ich nutze ResponseStatusException für einfache Fälle, in denen ich schnell einen bestimmten HTTP-Status wie 404 Not Found brauche. Für größere Anwendungen bevorzuge ich eigene Exceptions plus globales @RestControllerAdvice — einheitliches Fehlerformat und stärkere Domain-Bedeutung.


60. Kleines Code-Übungsbeispiel

Erstelle eine Exception:


public class ResourceNotFoundException extends RuntimeException {

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

Erstelle ein Error-DTO:


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

Erstelle einen globalen 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);
}
}

Fragen:

  1. Welche Annotation macht diesen Handler global?
  2. Welche Exception behandelt er?
  3. Welchen HTTP-Status liefert er?
  4. Warum HttpServletRequest nutzen?
  5. Warum ein Error-DTO nutzen?

Antworten:

  1. @RestControllerAdvice
  2. ResourceNotFoundException
  3. 404 Not Found
  4. Um den Request-Pfad zu lesen.
  5. Um eine einheitliche JSON-Fehlerantwort zurückzugeben.

61. Kleine Bug-Übung 1

Problem:


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

Frage:

Was ist falsch?

Antwort:

Es gibt rohe Exception-Details an den Client weiter. Das kann interne Klassennamen, Stack Traces, SQL-Details oder sensible Informationen preisgeben. Logge die Exception intern und gib eine sichere generische Meldung zurück.


62. Kleine Bug-Übung 2

Problem:


@ControllerAdvice
public class GlobalExceptionHandler {

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

Die REST API liefert nicht das erwartete JSON.

Frage:

Was könnte fehlen?

Antwort:

Einfaches @ControllerAdvice enthält nicht automatisch @ResponseBody. Für REST APIs nutze @RestControllerAdvice oder @ResponseBody auf der Handler-Methode.


63. Kleine Bug-Übung 3

Problem:


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

DTO:


public record CreateTaskRequest(
@NotBlank String title
) {
}

Die Validierung läuft nicht.

Frage:

Was fehlt?

Antwort:

@Valid fehlt.

Fix:


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

Übungsfragen

Frage 1

Warum brauchen REST APIs gutes Exception Handling?

Antwort:

REST APIs brauchen gutes Exception Handling für klare Statuscodes, einheitliche Error-Bodies, sichere Meldungen und nützliche Error-Codes — ohne interne Details preiszugeben.


Frage 2

Was macht @ExceptionHandler?

Antwort:

@ExceptionHandler markiert eine Methode, die bestimmte Exception-Typen behandelt, die von Controller-Methoden geworfen werden.


Frage 3

Was ist ein lokaler Exception Handler?

Antwort:

Ein lokaler Exception Handler ist eine @ExceptionHandler-Methode in einem Controller. Er behandelt Exceptions für diesen Controller.


Frage 4

Was macht @ControllerAdvice?

Antwort:

@ControllerAdvice liefert controller-übergreifendes Verhalten — üblicherweise globales Exception Handling.


Frage 5

Was enthält @RestControllerAdvice?

Antwort:

@RestControllerAdvice enthält @ControllerAdvice und @ResponseBody.


Frage 6

Welche Struktur ist gut für eine API-Fehlerantwort?

Antwort:

Eine gute API-Fehlerantwort kann timestamp, status, error, code, message, path und bei Validierungsfehlern field errors enthalten.


Frage 7

Warum solltest du keine Stack Traces an API-Clients geben?

Antwort:

Stack Traces können interne Klassennamen, SQL-Details, Dateipfade, Package-Namen und sensible Implementierungsdetails preisgeben.


Frage 8

Was macht @ResponseStatus?

Antwort:

@ResponseStatus setzt einen festen HTTP-Status für eine Controller-Methode oder Exception-Klasse.


Frage 9

Was ist ResponseStatusException?

Antwort:

ResponseStatusException ist eine Exception mit HTTP-Status und optionalem Grund — nützlich für einfache HTTP-Fehler.


Frage 10

Was ist ProblemDetail?

Antwort:

ProblemDetail ist eine Standardstruktur für HTTP-API-Fehlerantworten mit Feldern wie status, title, detail, type und instance.


Frage 11

Wie behandelst du Validierungsfehler von @Valid @RequestBody?

Antwort:

Behandle MethodArgumentNotValidException in einem globalen @RestControllerAdvice und liefere strukturiert 400 Bad Request mit Feldfehlern.


Frage 12

Welche Exception steht üblicherweise für fehlgeschlagene Request-Body-Validierung?

Antwort:

MethodArgumentNotValidException.


Frage 13

Welchen Status sollte ungültiges JSON zurückgeben?

Antwort:

Ungültiges JSON sollte üblicherweise 400 Bad Request zurückgeben.


Frage 14

Welchen Status sollten fehlende Ressourcen zurückgeben?

Antwort:

Fehlende Ressourcen sollten üblicherweise 404 Not Found zurückgeben.


Frage 15

Welchen Status können Business-Konflikte zurückgeben?

Antwort:

Business-Konflikte können 409 Conflict zurückgeben.


Frage 16

Was ist der Unterschied zwischen 401 und 403?

Antwort:

401 Unauthorized bedeutet: Authentifizierung fehlt oder ist ungültig. 403 Forbidden bedeutet: authentifiziert, aber nicht berechtigt.


Frage 17

Was ist ResponseEntityExceptionHandler?

Antwort:

ResponseEntityExceptionHandler ist eine praktische Basisklasse für viele eingebaute Spring-MVC-Exceptions.


Frage 18

Warum ist ein generischer Exception-Handler nützlich?

Antwort:

Ein generischer Exception Handler ist als letzter Fallback für unerwartete Fehler nützlich.


Frage 19

Was sollte ein generischer Exception Handler vermeiden?

Antwort:

Er sollte keine rohen Exception-Details, Stack Traces, SQL-Details, internen Klassennamen oder sensible Daten an den Client geben.


Frage 20

Warum ist globales Exception Handling besser als try-catch in jeder Controller-Methode?

Antwort:

Globales Exception Handling vermeidet wiederholte try-catch-Blöcke, hält Controller sauber und sorgt für ein einheitliches Fehlerantwort-Format.

Merksätze zum Mitnehmen

  • @ExceptionHandler behandelt bestimmte Exceptions.
  • Lokale Exception Handler gelten für einen Controller.
  • @ControllerAdvice gilt über Controller hinweg.
  • @RestControllerAdvice entspricht @ControllerAdvice plus @ResponseBody.
  • Nutze globales Exception Handling für einheitliche REST-API-Fehler.
  • @ResponseStatus setzt einen festen HTTP-Status.
  • ResponseStatusException ist nützlich für einfache HTTP-Fehler.
  • ProblemDetail ist eine Standard-Fehlerantwort-Struktur.
  • MethodArgumentNotValidException steht üblicherweise für fehlgeschlagenes @Valid @RequestBody.
  • Ungültiges JSON liefert üblicherweise 400.
  • Fehlende Ressourcen liefern üblicherweise 404.
  • Business-Konflikte liefern oft 409.
  • 401 bedeutet: nicht authentifiziert.
  • 403 bedeutet: authentifiziert, aber nicht berechtigt.
  • ResponseEntityExceptionHandler hilft bei eingebauten MVC-Exceptions.
  • Gib keine Stack Traces oder internen Details an Clients weiter.
  • Logge detaillierte Fehler intern.
  • Gib sichere, stabile, strukturierte Fehlerantworten zurück.