CodeGym /Kurse /Spring REST & MVC /Kategorien von Fehlern in der API

Kategorien von Fehlern in der API

Spring REST & MVC
Level 19, Lektion 1
Verfügbar

1. Fehlerkategorien und ihre Bedeutung

Wenn wir eine Konsolenanwendung „für mich selbst“ schreiben würden, könnten wir mit der Philosophie „ist abgestürzt – na ja, egal“ leben. Aber eine REST-API bedeutet immer die Interaktion mit jemandem von außen: Frontend, mobiler App, Integration, einem anderen Dienst. Und dieser „jemand“ muss verstehen, was passiert ist: er hat ungültige Daten gesendet, eine nicht vorhandene Ressource angefordert, einen Zustandskonflikt verursacht oder der Server ist über seine eigenen Füße gestolpert.

Der größte Schaden durch das Fehlen von Kategorien besteht darin, dass Fehler nicht unterscheidbar werden. Der Client beginnt anhand der Nachrichtenzeichenkette zu erraten, was passiert ist, oder zeigt dem Benutzer einfach „Fehler 500“ an und geht in den Sonnenuntergang. Und das Serverteam fängt später Bugs der Art ein: „Warum zeigt die UI ‚Server nicht erreichbar‘, wenn der Benutzer einfach vergessen hat, den Titel auszufüllen“. Spoiler: weil der Server nicht erklärt hat, dass es sich um einen Eingabefehler handelt.

Fehlerkategorien sind nicht der Versuch, aus Ihnen einen Liebhaber von Excel-Tabellen zu machen. Es ist eine Möglichkeit, sich auf eine Bedeutung zu einigen: Wenn der Fehler die Validierung betrifft, weiß der Client, dass er die Anfrage korrigieren muss; wenn der Fehler fachlich ist, versteht der Client, dass die Anfrage korrekt ist, die Operation aber durch Regeln oder den aktuellen Zustand verboten ist; wenn es eine technische Störung ist, bleibt dem Client nur, es später erneut zu versuchen oder „wir kümmern uns bereits darum“ anzuzeigen, weil der Server das Problem beheben muss.

Um die Idee kurz und bündig festzuhalten, halten wir uns an diese Stütze:

Kategorie Was bedeutet sie Wer ist dem Sinne nach „schuld“ Beispiel aus Task Tracker API
Eingabefehler (validation/Binding/Parsing) Die Anfrage entspricht nicht dem Vertrag Client (oder dessen UI) leerer title, ungültiger Enum status, defekter JSON
Fachlicher Fehler Die Anfrage ist korrekt, aber die Operation verletzt eine Domänenregel Client versucht eine verbotene Aktion Versuch, eine archivierte Aufgabe zu ändern
Technische Störung Der Server konnte das Szenario aufgrund eines internen Problems nicht korrekt ausführen Server/Infrastruktur Bug im Code, unerwarteter null, interner Repository-Fehler

Im Folgenden werde ich aus Gewohnheit manchmal validation error sagen, aber hier verwenden wir dieses Wort weit gefasst: es geht nicht nur um Bean Validation, sondern allgemein um Eingabe-/Vertragsfehler – @Valid, JSON-Parsing, Parameter-Binding und Type Mismatch.

Diese Unterteilung werden wir jetzt im Detail besprechen, denn „drei Wörter kennen“ und „im echten Code klassifizieren können“ sind zwei verschiedene Welten.

2. Wo Fehler in der API entstehen

Wenn man mit Spring MVC zu arbeiten beginnt, scheint alles „im Controller“ zu passieren. Als ob die Anfrage kommt und dann die ganze Welt Ihre Methode create() oder getById() ist. In Wirklichkeit finden vor dem Eintritt in den Controller bereits viele Schritte statt: Parameter werden geparst, der Body gelesen, Typen konvertiert, die Validierung gestartet. Und nach dem Controller kommen Services, Repositories und das ganze erwachsene Leben.

Um Fehler sicher in Kategorien einteilen zu können, ist es hilfreich, denselben vereinfachten Pipeline im Kopf zu behalten, aber jetzt als Karte zukünftiger Kategorien, nicht nur als Liste von Schritten.

flowchart TD
    %% Vereinfachte Pipeline der Anfrageverarbeitung in einer API
    A[HTTP-Anfrage] --> B["Binding path/query params"]
    B --> C["Body lesen und JSON -> DTO"]
    C --> D["Bean Validation @Valid"]
    D --> E[Controller-Methode]
    E --> F["Service-Layer: Geschäftsregeln"]
    F --> G["Repository/In-Memory-Storage"]
    G --> H[Antwort an den Client]

    %% Punkte, an denen am häufigsten „Eingabe fehlschlägt“
    B -->|Type Mismatch| X1[Eingabefehler]
    C -->|Malformed JSON| X1
    D -->|Constraint-Verletzungen| X1

    %% Punkte, an denen am häufigsten Domänenfehler und Serverfehler auftreten
    F -->|Nicht gefunden / Konflikt / verboten| X2[Fachlicher Fehler]
    G -->|Speicherfehler/Bug| X3[Technische Störung]
    F -->|Unerwartete Situation| X3

Beachten Sie eine wichtige Sache: Ein Fehler kann auftreten, ohne dass Ihr Code überhaupt betreten wird. Zum Beispiel sendet der Client status=DOING, aber unser Enum TaskStatus kennt das nicht. Spring versucht, die Zeichenkette in den Enum zu konvertieren und scheitert. Das ist kein „Fehler der Geschäftslogik“ und kein „Servicefehler“, sondern ein Fehler auf der Eingabeebene.

Gleiches gilt für malformed JSON. Sie können tausendmal if(request == null) schreiben, aber wenn der JSON defekt ist, kommt es gar nicht zu request: der Anfragebody konnte nicht geparst werden.

Andererseits gibt es Fehler, die prinzipiell nur in der Service-Schicht auftreten. Zum Beispiel „Die Aufgabe ist archiviert – sie kann nicht geändert werden“. Der Eingang kann perfekter JSON sein und alle Constraints können bestanden werden. Aber die Operation ist trotzdem durch eine fachliche Regel verboten. Das ist eine andere Kategorie.

Und schließlich gibt es Situationen, in denen „alles richtig ist“, aber der Server selbst kaputt geht: Bug, unerwarteter Zustand, interner Speicherfehler. Das ist eine technische Störung.

Als Nächstes gehen wir die drei Kategorien so durch, dass Sie sie nicht nur benennen, sondern sicher mit dem Finger darauf zeigen können: „Hier ist ein Eingabefehler“, „hier ist ein Geschäftsfehler“, „hier hat sich der Server selbst kaputt gemacht“.

3. Eingabefehler: wenn die Anfrage „am Eingang kaputt“ ist

Eingabefehler sind die Fälle, in denen die Anfrage in ihrer aktuellen Form nicht angenommen werden kann. Nicht „wir wollen nicht“ und nicht „wir haben eine solche Regel“, sondern genau „das entspricht nicht dem Vertrag des Endpunkts“. Hier ist es nützlich, als API-Entwickler zu denken: Sie haben dem Client eine bestimmte Datenform und bestimmte zulässige Werte versprochen, und der Client hat dieses Versprechen gebrochen.

Hierher gehören zwei sinngemäß verwandte Untertypen: Einschränkungsfehler (Bean Validation) und Format-/Konvertierungsfehler (malformed JSON, falsche Typen, falsche Enums, falsche Datumsangaben). Aus Sicht des Clients ist es derselbe Gedanke: „Ich habe eine ungültige Anfrage gesendet, ich muss sie korrigieren“. Nur die Quelle ist unterschiedlich: mal stolpert die Validierung, mal früher das Parsing oder die Typkonvertierung.

Beginnen wir mit dem, was bereits bekannt ist: Bean Validation auf dem DTO. Zum Beispiel muss eine Create-Anfrage für eine Aufgabe einen nicht leeren title mit einer Länge von 3 bis 120 haben.

package com.example.tasktracker.api.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

// DTO der Eingabeanfrage zum Erstellen einer Aufgabe.
// Hier legen wir den API-Vertrag fest: welche Pflichtfelder es gibt und welche Einschränkungen für sie gelten.
public record TaskCreateRequest(
        @NotBlank // title ist Pflicht: ein leerer String ist ein Eingabefehler (Validation)
        @Size(min = 3, max = 120) // zusätzlich wird die Länge eingeschränkt: das ist ebenfalls Teil des Endpunktvertrags
        String title
) {
}

Wenn der Client title: "" oder title: "ok" sendet, ist das kein „fachlicher Konflikt“ und kein „Server konnte nicht“. Es ist einfach, dass die Anfrage den Vertrag nicht erfüllt: wir haben vorher gesagt, dass title so sein muss. Das ist also ein typischer Eingabefehler.

Wichtiger praktischer Punkt: Eingabefehler leben an der API-Grenze. Das heißt, Sie sollten sie nicht über Services verteilen wie if(request.getTitle() == null) throw .... Im Idealfall sollte der Service eine Eingabe erhalten, die bereits grundlegende Formatprüfungen bestanden hat.

Auf der Controllerseite reicht in der Regel ein einziges @Valid:

import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@PostMapping("/api/v1/tasks")
public TaskDetailsResponse create(@Valid @RequestBody TaskCreateRequest request) {
    // @RequestBody: teilt Spring mit, JSON aus dem Body zu lesen und in das DTO zu konvertieren
    // @Valid: startet Bean Validation vor dem Eintritt in die Geschäftslogik
    return taskService.create(request); // in den Service geht eine „formal gültige“ Anfrage
}

Nun zur zweiten Hälfte: Format- und Typkonvertierungsfehler. Hier ist es sehr leicht, verwirrt zu werden, weil sie „nicht wie Ihre Constraints aussehen“. Zum Beispiel ruft der Client einen List-Endpunkt mit einem Filter auf:

GET /api/v1/tasks?status=DOING

Wenn DOING in Ihrem TaskStatus nicht existiert, liegt das Problem nicht in der Geschäftslogik. Der Server versteht einfach nicht, was ihm übergeben wurde, weil der Wert nicht in die zulässige Menge passt. Das ist die gleiche Natur des Fehlers wie @Size(min=3), nur an einer anderen Stelle der Pipeline.

Ein weiteres häufiges Beispiel sind Datumsangaben. Wenn Sie dueBefore=2026-03-01 erwarten, der Client aber dueBefore=01-03-2026 (oder dueBefore=gestern – ja, Leute können das) sendet, dann liegt das Problem im Format. Der Server kann die Zeichenkette aus dem Query-Parameter nicht korrekt mit LocalDate verknüpfen. Auch das ist ein Eingabefehler.

Und ein separates „Genre-Klassiker“ ist malformed JSON. Zum Beispiel sendet der Client:

{ "title": "Miete zahlen", }

Das Komma vor der schließenden geschweiften Klammer macht den JSON ungültig, und die Verarbeitung wird noch vor der Validierung abgebrochen. Das ist wichtig: Versuchen Sie nicht, das im Service „abzufangen“. Sie erhalten physisch kein DTO – es konnte nicht erstellt werden.

Zusammengefasst: Eingabefehler sind alles, was bedeutet: „Die Anfrage kann in ihrer aktuellen Form nicht angenommen werden, weil sie den Vertrag des Endpunkts verletzt“. Sie können in DTO-Constraints, URL-Parametern, JSON-Parsing, Typkonvertierung auftreten. Aber die Bedeutung für die API ist eine: die Eingabe ist ungültig.

4. Fachliche Fehler: korrekte Eingabe, aber nicht erlaubt

Ein fachlicher (Business-)Fehler ist der Moment, in dem die Anfrage anständig aussieht: JSON wird gelesen, DTO wird erstellt, Validierung läuft durch, Parameter werden konvertiert. Der Client könnte sogar denken: „So, jetzt muss der Server das machen“. Und der Server antwortet: „Nein, es ist nicht erlaubt, weil die Regeln der Domäne oder der aktuelle Zustand der Ressource es so vorsehen“.

Hier ist es besonders wichtig, nicht in „alles Validierungsfehler“ zu verfallen. Ein fachlicher Fehler hat eine andere Bedeutung. Es ist nicht „das Feld ist zu kurz“, sondern „die Operation kollidiert mit den Regeln“. Genau deshalb haben wir in den vorherigen Tagen die Grenze zwischen Input Validation und Business Validation eingeführt: Geschäftsregeln hängen normalerweise vom Systemzustand ab, und daher können sie nicht vollständig durch Annotationen auf dem Request-DTO ausgedrückt werden.

Ein klassisches Beispiel ist „Ressource nicht gefunden“. Angenommen, wir haben einen Endpunkt:

GET /api/v1/tasks/{taskId}

Die Anfrage kann perfekt geformt sein, taskId kann eine Zeichenkette im richtigen Format (und sogar eine gültige UUID) sein, aber die Aufgabe selbst ist im Speicher nicht vorhanden. Das ist kein Validierungsfehler, denn die Form der Anfrage ist korrekt. Es ist eine fachliche Situation: Der Client adressiert eine Ressource, die im aktuellen Systemzustand nicht existiert.

Im Code sollte eine solche Situation explizit benannt werden, nicht als null zurückgegeben werden. Zum Beispiel:

public Task getById(String taskId) {
    // Ressource anhand der ID suchen: taskId kann „gültig“ sein, aber Daten können fehlen.
    Task task = taskRepository.findById(taskId);

    if (task == null) {
        // Das ist ein fachlicher Grund (Ressource nicht vorhanden), kein Formatfehler der Eingabe.
        throw new TaskNotFoundException(taskId);
    }

    return task; // Hier garantieren wir dem aufrufenden Code, dass die Task existiert.
}

Hier erscheint ein wichtiger Akteur dieses Modells: die benannte anwendungsspezifische Ausnahme. TaskNotFoundException sagt dem Leser des Codes: „Das ist keine zufällige NPE und kein abstraktes 'irgendwas ist schief gelaufen', sondern ein konkreter Grund – die Aufgabe existiert nicht“. Es ist nützlich, wenn eine solche Ausnahme auch taskId speichert: dieser Kontext wird später für das öffentliche Error-Payload benötigt.

Ein weiteres Beispiel für einen fachlichen Fehler ist „Aufgabe ist archiviert und kann nicht geändert werden“. Das ist eine Domänenregel: die Eingabe kann korrekt sein, aber die Operation ist verboten. Wir können das so ausdrücken:

public void assertTaskNotArchived(Task task) {
    // Domänenregel prüfen: „archivierte Aufgabe ist unveränderbar“.
    // Das betrifft nicht die Form der Anfrage, sondern den Zustand der Ressource.
    if (task.getStatus() == TaskStatus.ARCHIVED) {
        throw new TaskArchivedException(task.getId());
    }
}

Und wieder: Die Anfrage kann ideal sein, title schön, Tags eindeutig, aber die Regel sagt: „archivierte Aufgabe nicht bearbeiten“. Das ist kein Formfehler der Anfrage, sondern ein Konflikt der Operation mit dem Ressourcenzustand.

Beachten Sie ein wichtiges Architekturdetail: Solche Prüfungen leben normalerweise in der Service-Schicht (oder in der Domänenschicht), nicht im Controller. Der Controller sollte dünn sein: er nimmt den Vertrag entgegen und delegiert die Ausführung des Szenarios. Wenn Sie beginnen, fachliche Regeln in jeden Controller zu schreiben, bekommen Sie schnell zwei Unannehmlichkeiten: Regeln beginnen sich zu duplizieren und das Verhalten wird zwischen den Endpunkten inkonsistent.

Ein weiterer typischer Business-Konflikt ist ein verbotener Statusübergang. Wenn die Domäne zum Beispiel sagt, dass man von DONE nicht zurück zu TODO kann, dann kann die Anfrage „Status ändern“ korrekt sein, aber die Aktion ist verboten. Im Code wird daraus ein sinnvoller Grund:

public void changeStatus(Task task, TaskStatus newStatus) {
    // Beispiel einer Domäneneinschränkung für Statusübergänge.
    // Die Eingabe kann korrekt sein (gültiger Enum), aber die Operation ist durch Regeln verboten.
    if (task.getStatus() == TaskStatus.DONE && newStatus == TaskStatus.TODO) {
        throw new InvalidStatusTransitionException(task.getId(), task.getStatus(), newStatus);
    }

    task.setStatus(newStatus); // Wenn die Regel nicht verletzt ist, ändern wir den Zustand.
}

Ja, dieses Szenario wird sich im Laufe des Projekts weiterentwickeln, aber als Beispiel für eine Klassifizierung ist es ideal: Die Eingabe ist korrekt, aber die Regel verbietet es.

Insgesamt gilt für fachliche Fehler: „Zerstöre nicht den Vertrag der Anfrage, aber versprich dem Client nicht, dass er alles tun kann, was ihm einfällt“. Eine API ist nicht nur die Form, sondern auch die Bedeutung der Operationen.

5. Technische Störungen auf Serverseite

Eine technische Störung ist ein Fall, in dem dem Sinne nach alles hätte funktionieren sollen, der Server das Szenario aber aufgrund eines internen Problems nicht korrekt abschließen konnte. Manchmal ist es ein Bug im Code (und ja, die gibt es sogar bei Leuten, die „alles genau geprüft haben“). Manchmal ist es ein Infrastrukturproblem: unerwarteter Dateisystemfehler, Speichermangel, Serialisierungsfehler, interne Inkonsistenz der Daten.

Hier ist es nützlich, einen einfachen Gedanken im Kopf zu behalten: Wenn der Client nichts an der Anfrage korrigieren kann, weil die Anfrage in Ordnung ist, und der Fehler gleichzeitig keine fachliche Regel beschreibt, dann handelt es sich fast sicher um ein technisches Problem des Servers.

Das verständlichste Lernbeispiel ist die Verletzung interner Invarianten. Angenommen, Sie erwarten, dass eine Aufgabe nach dem Speichern immer eine id hat. In unserem Projekt wird id serverseitig generiert, der Client verwaltet sie nicht. Wenn aus irgendeinem Grund id nicht gesetzt wurde, ist das ein Serverproblem.

public TaskDetailsResponse create(TaskCreateRequest request) {
    // Neue Aufgabe speichern. Der Client verwaltet id nicht – das ist die Verantwortung des Servers/Speichers.
    Task saved = taskRepository.save(mapper.toModel(request));

    if (saved.getId() == null) {
        // Interne Invariante verletzt: nach save() muss id existieren.
        // Das ist kein Client-Fehler und keine Domänenregel, sondern eine technische Störung.
        throw new IllegalStateException("Saved task has no id");
    }

    return mapper.toDetails(saved); // Modell in DTO der Antwort umwandeln.
}

Beachten Sie: Das ist keine „fachliche Regel“. Die Domäne sagt nicht: „Aufgabe kann manchmal ohne id sein“. Es ist einfach unsere interne Logik kaputtgegangen. Deshalb werfen wir IllegalStateException: als Entwickler erklären wir „dieser Zustand ist unzulässig“.

Ein weiteres Beispiel ist ein versehentlicher null, der nicht null hätte sein sollen. Zum Beispiel erhält ein Mapper eine Task und plötzlich ist innerhalb der Task das Feld title null, obwohl das System das hätte garantieren sollen. Je nachdem, wo die Garantie hätte greifen sollen, kann das entweder ein Bug (technisch) oder ein Loch in der Validierung sein (dann haben Sie falsch klassifiziert und einen Validation Error verpasst). Aber wenn Sie bereits sicher sind, dass title erforderlich ist, und im Modell ist er null, dann ist das mit ziemlicher Sicherheit ein technisches Problem.

Technische Störungen von fachlichen Fehlern zu unterscheiden, ist besonders wichtig aus einem Grund: Sie sollten nicht zu einem „normalen Verhaltenszweig“ werden. Wenn Sie anfangen, Code zu schreiben wie „wenn ein NullPointerException auftritt, gib dem Client JSON 'irgendwas ist schief gelaufen' zurück und mach weiter“, machen Sie das System unmerklich zerbrechlich und nicht testbar. Eine technische Störung ist ein Signal: „auf dem Server reparieren“.

Und noch ein Punkt: Technische Störungen sollten sich nicht als Validierung oder fachlicher Fehler „tarnen“. Manchmal möchte man dem Client sagen „na, Sie haben wohl etwas Falsches gesendet“, weil das einfacher ist. Aber wenn die Wahrheit ist, dass der Server aufgrund eines Bugs abgestürzt ist, sollte man es ehrlich als technische Störung betrachten. In der nächsten Vorlesung werden wir über Status sprechen, aber die Bedeutung ist jetzt schon: verschiedene Fehlerkategorien sollten unterschiedliche semantische Antworten haben, sonst lebt der Client im Modus „rate mal“.

6. Grenzfälle: ein schneller Algorithmus

In der Praxis kommen Fehler nicht mit einem Schild „ich bin Validation“ oder „ich bin Business“. Sie kommen als Fakten: die Anfrage wurde nicht verarbeitet, irgendwo wurde eine Ausnahme geworfen, irgendein Schritt der Pipeline ist kaputtgegangen. Daher ist es nützlich, einen kleinen Klassifikationsalgorithmus zu haben, den man im Kopf durchgehen kann, ohne einen philosophischen Traktat aufzuschlagen.

Der praktikabelste Weg ist, sich die Fragen in der richtigen Reihenfolge zu stellen. Ganz vereinfacht prüfen wir zuerst: „Kann die Anfrage überhaupt gelesen und mit dem Vertrag verknüpft werden?“, dann „Kann die Operation gemäß den Domänenregeln ausgeführt werden?“, und erst dann „Ist etwas Unerwartetes auf dem Server passiert?“

Unten ist ein Schema, das man gut im Kopf behalten kann (und ja, man kann es auf einen Aufkleber zeichnen; niemand wird es erfahren, außer Ihrer Katze).

flowchart TD
    A[Anfrage angekommen] --> B{"Kann gelesen und mit Vertrag verknüpft werden?"}
    B -->|"nein: JSON/Typen/Pflichtfelder"| V[Validation Error]
    B -->|ja| C{"Ist die Operation durch Domänenregeln und Zustand der Ressource erlaubt?"}
    C -->|nein| D[Business Error]
    C -->|ja| E{"Hat der Server das Szenario korrekt ausgeführt?"}
    E -->|"nein: Bug/Störung"| T[Technical Error]
    E -->|ja| OK[Success]

Jetzt besprechen wir die häufigsten „Fallstricke“:

Bei malformed JSON ist es einfach: Wenn der Anfragebody nicht geparst werden konnte, ist das weder Business noch Technical. Es ist ein Eingabefehler, also dem Sinne nach Validation. Hier muss der Client die Anfrage korrigieren, sonst kann der Server die Daten physisch nicht verstehen.

Bei fehlender Ressource wird oft verwechselt. Man denkt: „Na, wenn taskId falsch ist, dann ist das Validation“. Aber nein: taskId kann eine völlig korrekte ID sein, nur die Ressource existiert nicht. Das ist ein Fehler über den Systemzustand. Es geht nicht um die Form der Anfrage, sondern um fehlende Daten.

Bei Verboten wie „archivierte Aufgabe kann nicht geändert werden“ wird auch oft verwechselt, besonders wenn Anfänger an den Gedanken gewöhnt sind „alles mit Annotationen prüfen“. Aber diese Regel betrifft nicht die Struktur der Eingabe, sondern die Bedeutung der Operation im aktuellen Zustand. Also ist es ein fachlicher Fehler.

Eine technische Störung hingegen ist das, was weder in „Eingabe schlecht“ noch in „Domänenregel verbietet“ passt. Wenn Sie plötzlich eine IllegalStateException haben, die bedeutet „unmöglicher Zustand im System“, ist das kein Grund, den Client zu beschuldigen. Es ist ein Grund, den Code zu reparieren.

Eine nützliche „Spickzettel“ nach Merkmalen (ohne in Listen zu verfallen) sieht so aus: Wenn der Fehler auf ein bestimmtes Feld der Anfrage und seine Zulässigkeit hinweist, ist es fast immer Validation. Wenn der Fehler von einer bestimmten Ressource, ihrem Vorhandensein und Zustand spricht, ist es meistens Business. Wenn der Fehler „unmöglicher Zustand“, unexpected, null dort, wo kein null sein sollte, IO error sagt, ist es technisch.

Wenn eine solche Klassifizierung etabliert ist, hört die Wahl des Status auf, ein Ratespiel zu sein: 400, 404, 409 und 500 bekommen endlich eine Stütze in der Bedeutung, nicht in der Laune des Autors des Controllers.

7. Benannte Ausnahmen und Kategorien im Code

Ein sehr häufiges Problem in Anfängerprojekten sind Ausnahmen ohne Bedeutung. Zum Beispiel throw new RuntimeException("Bad request"). Eine solche Zeile im Code erklärt nichts: es ist nicht klar, ob es um Validierung, eine fachliche Regel, das Fehlen einer Ressource oder einen internen Bug geht. Am Ende geht die Fehlerkategorie verloren, und damit auch der korrekte HTTP-Status und das normale Antwortformat.

Benannte anwendungsspezifische Ausnahmen lösen dieses Problem: Sie verwandeln „irgendwas ist schief gelaufen“ in eine konkrete Ursache, die konsistent behandelt werden kann. In unserem Kurs leben sie im Paket com.example.tasktracker.domain.exception, weil sie hauptsächlich fachliche Gründe ausdrücken (not found, archived, conflict). Das ist eine wichtige Trennung: DTO und Validation liegen in api, die sinnhaften Gründe in domain.

Das Beispiel Aufgabe nicht gefunden haben wir bereits als benannte anwendungsspezifische Ausnahme mit taskId darin. Wichtig ist hier die Kategorie: das ist ein fachlicher Grund Ressource nicht vorhanden.

Innerhalb einer solchen Ausnahme kann die Nachricht vorerst „roh“ sein. Und das ist in Ordnung. Jetzt ist wichtig, dass der Grund einen Namen bekommen hat und wir ihn im Code nicht mit „archivierte Aufgabe“ oder „technische Störung“ verwechseln.

Hier ist ein Beispiel für die Ausnahme „Aufgabe ist archiviert“:

package com.example.tasktracker.domain.exception;

// Fachlicher Grund: Operation aufgrund des Ressourcenzustands (ARCHIVED) verboten.
public class TaskArchivedException extends RuntimeException {
    public TaskArchivedException(String taskId) {
        // Nachricht ist intern; später werden wir Gründe auf ein einheitliches API-Fehlerformat abbilden.
        super("Task is archived: " + taskId);
    }
}

Jetzt sind TaskNotFoundException und TaskArchivedException beide Domänenfehler, aber mit unterschiedlichen Bedeutungen. Und das hilft bereits bei der Klassifizierung: not found – Fehlen der Ressource, archived – Verbot der Operation aufgrund des Ressourcenzustands.

Es gibt zwei wichtige Regeln, die helfen, die Ausnahmen nicht in einen Zoo zu verwandeln.

Die erste Regel: Man sollte nicht für jede Codezeile eine eigene Ausnahme anlegen. Wir schreiben keine Enzyklopädie der Ausnahmen, wir fixieren stabile Gründe, die für den externen API-Vertrag wirklich wichtig sind. Task not found ist ein wichtiger Grund. Mapper got null task ist meistens ein interner Bug und kann eine technische Ausnahme bleiben.

Die zweite Regel: Die Ausnahme sollte die Bedeutung ausdrücken, nicht die Antwortstrategie. Das heißt, TaskNotFoundException ist ein guter Name, aber Task404Exception ist schlecht. Warum? Weil wir noch nicht einmal bis zum Mapping von Status in Vorlesung 3 gekommen sind und bereits HTTP in die Domänenschicht gezogen haben. Fachliche Gründe sollten fachliche Gründe bleiben; HTTP ist die Transportschicht, die wir darauf anwenden werden.

Und ja, Validierungsfehler, die von Bean Validation oder Spring MVC Binding erzeugt werden, versehen wir normalerweise nicht mit eigenen Ausnahmen. Sie sind auf Framework-Ebene bereits „benannt“. Unsere Aufgabe ist es, ihre Kategorie zu verstehen und später in ein einheitliches Antwortmodell zu überführen, aber keine manuelle Verarbeitung in den Controllern zu vermehren.

8. Typische Fehler bei der Fehlerklassifizierung

Fehler Nr. 1: anzunehmen, dass „jeder Fehler = Validation“ ist.
Das sieht verlockend aus: Annotationen angebracht, und alles andere als „invalid request“ bezeichnet. Aber am Ende unterscheidet der Client nicht zwischen „Ihr Titel ist leer“, „Aufgabe existiert nicht“ und „Archivierte Aufgabe kann nicht geändert werden“. Die API wird unlesbar: zwar alles 4xx, aber null Bedeutung.

Fehler Nr. 2: das Fehlen einer Ressource als Validierungsfehler zu bezeichnen.
Wenn die Anfrage korrekt ist, aber die Daten im System nicht vorhanden sind, ist das kein „schlechtes Format“, sondern ein Domänenzustand. Sie können das nicht durch „kleine Änderung am JSON“ beheben. Der Client muss verstehen, dass die adressierte Ressource nicht existiert, nicht dass er „Constraints nicht bestanden“ hat.

Fehler Nr. 3: fachliche Konflikte als technische Fehler zu verstecken.
Wenn eine Domänenregel die Operation verbietet, Sie stattdessen aber eine allgemeine RuntimeException werfen und einen „Serverfehler“ erhalten, wird der Client denken, dass „der Server kaputt ist“, obwohl er eigentlich nur etwas Verbotenes verlangt hat. Das zerstört die UX und behindert das Debuggen von Szenarien.

Fehler Nr. 4: einen internen Bug als Client-Problem auszugeben.
Manchmal möchte man das System „beruhigen“ und „ungültige Anfrage“ zurückgeben, obwohl man tatsächlich einen NullPointerException aufgrund eines Bugs hat. Damit beraubt man sich der Chance, das Problem ordentlich zu beheben: in den Logs herrscht Chaos, der Client wird versuchen zu erraten, wie man die Anfrage „richtig sendet“, und der Server stürzt weiter ab.

Fehler Nr. 5: eine gemeinsame allgemeine Ausnahme BusinessException für alles zu verwenden.
Man hat zwar einen Schritt nach vorne gemacht – Business von Technical getrennt. Aber wenn im Business-Korb sowohl not found als auch archived als auch invalid status transition als auch comment not found liegen, verlieren Sie wieder die Bedeutung. Benannte Ausnahmen gibt es genau dafür, dass die Bedeutung unterscheidbar ist.

1
Aufgabe
Spring REST & MVC, Level 19, Lektion 1
Gesperrt
Kategorieklassifizierung nach Ausnahmetyp
Kategorieklassifizierung nach Ausnahmetyp
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION