Packen Sie einen fetch()-Aufruf in ein try/catch, rufen Sie einen Endpunkt auf, der mit 404 antwortet, und der catch-Block läuft nie. Das Promise wird aufgelöst. res.json() parst, was auch immer der Server zurückgeschickt hat, oft ein Fehler-Payload wie {"error": "user not found"}, und der Rest der Funktion behandelt es als den angeforderten Datensatz. Keine Exception, keine Konsolenwarnung. Die falschen Daten wandern weiter, bis etwas anderes daran zerbricht.
| |
Dieses catch greift nur bei einer Anfrage, die nie abgeschlossen wird. Alles andere braucht eine Prüfung, die Sie selbst schreiben müssen.
Warum behandelt fetch() 404 und 500 als Erfolg?
fetch() wird abgelehnt, wenn die Anfrage selbst fehlschlägt: die DNS-Auflösung schlägt fehl, die Verbindung bricht mitten in der Übertragung ab, eine CORS-Richtlinie blockiert die Antwort, bevor sie Ihren Code erreicht, das Netzwerk des Nutzers fällt aus. In keinem dieser Fälle kommt bei Ihnen überhaupt eine Serverantwort an.
Ein 404 oder ein 500 bedeutet: Der Server hat geantwortet. Die Verbindung hat funktioniert, eine Antwort mit Headern und Body ist zurückgekommen. Der Austausch ist abgeschlossen, nur mit einer schlechten Nachricht. Die Fetch-Spezifikation wertet das als erfolgreiches fetch und überlässt Ihnen die Prüfung des Statuscodes. response.ok ist true für jeden Status im Bereich 200–299 und false für alles andere, einschließlich 4xx/5xx-Antworten und Redirects, denen fetch nicht gefolgt ist.
Das ist das Detail, das jedem zum Verhängnis wird, der von Axios kommt, das standardmäßig jeden Nicht-2xx-Status ablehnt. Keines von beidem ist falsch. Die Fetch API lehnt nur bei Fehlern auf Transportebene ab, weil ein 404 im Web nichts Außergewöhnliches ist. Erwarten Sie von fetch() aber die Semantik von Axios, bauen Sie sich genau den Bug von oben ein.
Wie prüfen Sie response.ok und werfen Ihren eigenen Fehler?
Ein if, platziert bevor Sie den Body berühren:
| |
Für ein Skript reicht das. Bei mehr als einer Aufrufstelle führt das Wiederholen dieser Zeilen überall dazu, dass irgendwann eine davon übersprungen wird. Packen Sie es einmal ein:
| |
Jeder Aufrufer bekommt jetzt eine echte Exception bei einem schlechten Status, mit Statuscode und dem Response-Body des Servers im Gepäck.
Wie lesen Sie den Fehler-Body, wenn er kein JSON ist?
Ein 500 von Ihrer eigenen API kommt wahrscheinlich als JSON zurück. Ein 502 von einem Reverse Proxy oder eine Sperrseite von einer WAF kommt meist als HTML zurück. Rufen Sie res.json() darauf auf, und Sie erhalten einen SyntaxError (“Unexpected token ‘<’”), der mit dem eigentlichen Fehler nichts zu tun hat und Sie das Falsche debuggen lässt.
Das fetchJSON von oben vermeidet das, indem es den Fehler-Body mit res.text() liest, unabhängig davon, ob der Inhalt gültiges JSON ist. Wollen Sie das geparste Objekt für den Fall, dass es tatsächlich JSON ist, prüfen Sie zuerst den Content-Type:
| |
Der Body einer Response lässt sich nur einmal lesen. Wenn Sie den Rohtext zum Loggen und das geparste JSON für den Aufrufer brauchen, rufen Sie zuerst res.clone() auf und lesen Sie je eine Kopie. Das Original zweimal zu lesen, wirft “body stream already read.”
Wie unterscheiden Sie einen Netzwerkfehler von einem HTTP-Fehler?
Sobald fetchJSON in beiden Fällen wirft, muss ein Aufrufer, der unterschiedlich reagieren will, prüfen, welchen der beiden er erhalten hat:
| |
Ein Netzwerkfehler erscheint als einfacher TypeError ohne Statuscode, weil es nie eine Antwort gab, aus der sich einer ablesen ließe. Die Meldung variiert je nach Browser: “Failed to fetch” in Chrome, “NetworkError when attempting to fetch resource” in Firefox. Unterscheiden Sie also nach dem Typ, nicht nach der Meldung.
Ein Fall sieht wie ein Netzwerkfehler aus, ist es aber nicht. Wenn eine Cross-Origin-Anfrage mit diesem TypeError fehlschlägt, während die Server-Logs zeigen, dass sie angekommen ist und mit 200 geantwortet hat, hat der Browser die Antwort blockiert, bevor Ihr Code sie gesehen hat. Die Korrektur liegt in den CORS-Headern des Servers, nicht in der Fehlerbehandlung des Clients. Was diese Header sagen müssen, erklärt Was ist CORS und wie behebt man CORS-Fehler.
Wie fügen Sie fetch() einen Timeout hinzu?
fetch() hat keinen Timeout. Es wartet so lange, wie die Verbindung offen bleibt, sodass ein Backend, das hängt statt einen Fehler zurückzugeben, die Anfrage unbegrenzt offen hält. AbortSignal.timeout() schneidet sie ab:
| |
Nach 5 Sekunden bricht die Anfrage ab, und das Promise wird mit einem TimeoutError abgelehnt, verschieden vom AbortError, den Sie bei manuellem Abbruch einer Anfrage bekommen:
| |
AbortSignal.timeout() erreichte 2024 Baseline-Verfügbarkeit, also Unterstützung in allen aktuellen gängigen Browsern. Für einen älteren Browser oder eine ältere Laufzeitumgebung bauen Sie dasselbe mit setTimeout und einem AbortController nach:
| |
Dieser Fallback lehnt mit AbortError ab, nicht mit TimeoutError, also muss eine Prüfung wie oben beide Namen akzeptieren.
Wenn der Aufrufer aus eigenen Gründen ebenfalls abbrechen muss (der Nutzer hat den Dialog geschlossen, eine neuere Anfrage hat diese ersetzt), kombinieren Sie beide Signale mit AbortSignal.any([ownSignal, timeoutSignal]), statt eines auszuwählen. AbortSignal.any() ist neuer als timeout(), prüfen Sie also, ob Ihre Ziel-Laufzeitumgebungen es unterstützen.
fetch vs Axios: Was wirft bei einem 404?
fetch() | Axios | |
|---|---|---|
| 4xx-/5xx-Antwort | Wird aufgelöst; response.ok prüfen | Wird abgelehnt; landet im catch |
| Response-Body | .json() / .text() selbst aufrufen | Automatisch in response.data geparst |
| Request-Timeout | Keiner eingebaut; AbortSignal.timeout() nutzen | Konfigurationsoption timeout |
| Netzwerk-/CORS-Fehler | Lehnt mit TypeError ab | Lehnt mit einem AxiosError ab |
fetch() ist in jede moderne Laufzeitumgebung eingebaut, da gibt es nichts zu installieren. Axios liefert Fehler-bei-schlechtem-Status und automatisches JSON-Parsing von Haus aus, auf Kosten einer Abhängigkeit. Wenn die Codebasis Axios ohnehin schon für Interceptoren oder Abbruch nutzt, verwenden Sie dessen Standardeinstellungen. Wenn nicht, gibt Ihnen der fetchJSON-Wrapper von oben dasselbe Verhalten in etwa zwanzig Zeilen.
Welche Fehler führen dazu, dass fetch()-Fehler unbehandelt bleiben?
response.oknach dem Aufruf von.json()prüfen. Zu diesem Zeitpunkt ist der Body bereits verbraucht, und bei einer HTML-Fehlerseite hat das Parsen schon eine Exception geworfen. Erstokprüfen, dann den Body lesen.- Eine asynchrone Funktion aus einem Event-Handler ohne
awaitoder.catch()aufrufen.onClick={() => fetchJSON(url)}verliert die Ablehnung: Kein catch-Block läuft, und je nach Laufzeitumgebung bekommen Sie eine Unhandled-Rejection-Warnung oder gar nichts. Wohin ein abgelehntes Promise in diesem Fall verschwindet, erklärt Async/Await und die Event Loop in JavaScript. - Ein
catch, das loggt undundefinedzurückgibt.catch (err) { console.error(err); }schluckt den Fehler. Der Aufrufer erhältundefined, wo er Daten erwartet hat, und stürzt woanders ab, weiter weg von der eigentlichen Ursache. - Annehmen, dass jeder Nicht-2xx-Body JSON ist. Das ist die übliche Ursache für “mein Error Handler hat einen anderen Fehler geworfen”. Mit
.text()lesen oder zuerstcontent-typeprüfen.
Wo gehört die response.ok-Prüfung hin?
In genau einem Modul. fetch() macht Schweigen zum Standardverhalten, also reproduziert jeder Aufruf ohne explizite Statusprüfung den Bug vom Anfang dieses Artikels. Schreiben Sie fetchJSON und HttpError einmal, fügen Sie die Timeout- und Content-Type-Behandlung von oben hinzu, und machen Sie es zum einzigen Weg, über den Ihre App mit einem Server spricht. Eine Lint-Regel oder eine Code-Review-Gewohnheit, die nackte fetch()-Aufrufe außerhalb dieses Moduls meldet, hält diese Disziplin aufrecht.