Entwurf für fehlgeschlagene Anfragen in Angular: Zustände, Interceptor und Wiederholungsversuche
Erfahren Sie, wie Sie HTTP-Fehler in Angular klassifizieren, den Ladezustand bereinigen, die Verarbeitung in Interceptors zentralisieren, sicher erneut versuchen und Benutzern Nachrichten anzeigen können, auf die sie reagieren können.
Die meisten Angular-Komponenten werden für den „happy path“ entwickelt: 200 OK, Daten in einer Eigenschaft, Template wird dargestellt. In der Produktion brechen Verbindungen ab, Sessions verfallen, Backends kollabieren und Benutzer klicken zweimal auf die Absenden-Buttons. Die unten beschriebenen Muster ermöglichen es einer Angular-Anwendung, solche Fehler zu erkennen, dem Benutzer eine Erklärung zu geben, dort wiederherzustellen, wo es sicher ist, und den Entwicklern genügend Informationen zur Behebung der Ursache zu liefern.
Der Preis für die Ignorierung des Fehler-Callbacks
Eine typische Komponentenanfrage abonniert sich, weist die Antwort zu und tut sonst nichts.
this.http.get('/api/accounts').subscribe({
next: (response) => {
this.accounts = response;
}
});
Falls der Server mit 500 antwortet, passiert nichts Sichtbares. Die Seite bleibt leer oder ein Ladeindikator dreht sich weiter, und die einzige Spur ist eine Konsoleintragung, die der Benutzer niemals öffnet.
Die minimale Verbesserung besteht in einem error-Handler, der den Fehler protokolliert und eine für das Template lesbare Nachricht speichert.
this.http.get('/api/accounts').subscribe({
next: (response) => {
this.accounts = response;
},
error: (error) => {
console.error('Failed to load accounts', error);
this.errorMessage = 'Unable to load accounts. Please try again.';
}
});
Das funktioniert für einen einzigen Bildschirm. Bei Dutzenden von Bildschirmen führt das Kopieren des Handlers überall zu inkonsistenten Nachrichten, vergessenen Randfällen und doppeltem Code – deshalb sind wiederverwendbare Muster wichtig.
Nicht alle Fehler bedeuten dasselbe
Der Statuscode gibt in der Regel an, wer das Problem beheben kann, und bestimmt so die Reaktion.
400 Bad Request: Die Eingabe ist falsch
Die Frontend-Seite hat ungültige Daten gesendet.
400 Bad Request
Der Benutzer kann das in der Regel selbst beheben, zum Beispiel indem er ein Formfeld korrigiert.
401 Unauthorized: Die Sitzung fehlt oder ist abgelaufen
Die Anmeldeinformationen fehlen, meist weil das Zugriffstoken abgelaufen ist.
401 Unauthorized
Die übliche Reaktion ist eine Umleitung zur Anmeldung oder zunächst die Erneuerung des Tokens.
403 Forbidden: Authentifiziert, aber nicht erlaubt
Der Server kennt den Benutzer, lehnt die Aktion aber ab.
403 Forbidden
Das ist eine Routine mit rollenbasiertem Zugriffssteuerungssystem. Es hilft nichts, den Benutzer zur Anmeldung zu schicken; er muss erfahren, dass ihm die Berechtigung fehlt.
404 Nicht gefunden: Die Ressource existiert nicht
Die Einheit wurde gelöscht oder die URL ist veraltet.
404 Not Found
500 Interner Serverfehler: Der Backend-Server ist ausgefallen
Etwas ist auf dem Server fehlgeschlagen.
500 Internal Server Error
Der Benutzer kann das nicht beheben; bieten Sie eine klare Nachricht sowie die Möglichkeit, später erneut zu versuchen.
Netzwerkfehler: Die Anfrage ist nie angekommen
Mannchmal erreicht die Anfrage den Server überhaupt nicht.
Internet connection lost
Server unavailable
DNS failure
Request timeout
Angular meldet diese als HttpErrorResponse mit einem status von 0, wodurch sie von echten Serverantworten unterschieden werden. Diese Kategorie bestimmt alles Weitere: ob wiederholt werden soll, umgeleitet wird und was dem Benutzer mitgeteilt wird.
Modellieren Sie die Anfrage als eine Reihe von Zuständen
Es ist ein häufiger Fehler, nur den Erfolg zu verfolgen. Behandeln Sie eine Anfrage als kleine Zustandsmaschine: Sie beginnt mit dem Laden und endet entweder erfolgreich oder fehlerhaft.
Loading
↓
Success
or
Loading
↓
Error
In einem Komponenten bedeutet das explizite Flags für den Ladezustand und die Fehlermeldung, die in jedem Ablaufpfad aktualisiert werden.
loading = false;
errorMessage = '';
loadAccounts() {
this.loading = true;
this.errorMessage = '';
this.http.get('/api/accounts').subscribe({
next: (data) => {
this.accounts = data;
this.loading = false;
},
error: () => {
this.errorMessage =
'Unable to load accounts. Please try again.';
this.loading = false;
}
});
}
Der entscheidende Punkt ist das Zurücksetzen von loading im Fehler-Callback; ohne dieses Zurücksetzen bleibt bei jedem Fehler ein endloser Ladeindikator sichtbar. Das Löschen von errorMessage zu Beginn verhindert außerdem, dass alte Fehler neben frischen Daten weiterhin angezeigt werden.
Lassen Sie finalize() das Ladeflag bereinigen
Das Zurücksetzen des Flags an zwei Stellen ist fehleranfällig. RxJS finalize() bietet eine zentrale Stelle für die Bereinigung.
import { finalize } from 'rxjs';
loadAccounts() {
this.loading = true;
this.http.get('/api/accounts')
.pipe(
finalize(() => {
this.loading = false;
})
)
.subscribe({
next: (data) => {
this.accounts = data;
},
error: () => {
this.errorMessage =
'Unable to load accounts.';
}
});
}
Der finalize()-Callback wird ausgeführt, wenn das Observable abgeschlossen ist oder Fehler auftritt, sowie wenn die Abonnementverbindung frühzeitig beendet wird. Dadurch eignet er sich ideal zum Verbergen von Ladeindikatoren, und die duplizierten Zeilen this.loading = false verschwinden.
Gemeinsames Verhalten in einem HTTP-Interceptor zentralisieren
Ausgelaufene Sessions oder allgemeine Ausfälle sollten auf jeder Seite gleich behandelt werden; ihre Implementierung pro Komponente ist daher wiederholend und anfällig. Ein HTTP-Interceptor sieht jede HttpClient-Anfrage, wodurch er der richtige Ort für Richtlinien auf Anwendungs-Ebene ist. Dieser Interceptor verwendet den funktionalen Stil von HttpInterceptorFn und prüft den Status der Anfragen.
export const errorInterceptor: HttpInterceptorFn =
(req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
console.log('Session expired');
}
if (error.status === 403) {
console.log('Access denied');
}
if (error.status >= 500) {
console.log('Server error');
}
return throwError(() => error);
})
);
};
Die Logging-Aufrufe dienen als Platzhalter für eine Umleitung, eine Benachrichtigung oder ein Überwachungsevent. Noch wichtiger ist, dass der Interceptor mit throwError(() => error) endet, sodass der Fehler den aufrufenden Component trotzdem erreicht. Der Interceptor kümmert sich um die gemeinsamen Aspekte, der Component um das, was nur er versteht – diese Trennung sorgt für eine wartbare Struktur. Funktionale Interceptors werden mit provideHttpClient(withInterceptors([...])) registriert.
Technische Fehler in verständliche Botschaften übersetzen
Backends liefern nicht immer freundliche Texte:
{
"message": "NullPointerException at AccountService.java:142"
}
Das hilft den Servicebetreibern, zeigt dem Kunden aber nur Verwirrung und enthüllt Implementierungsdetails. Zeigen Sie stattdessen etwas, das für den Benutzer geschrieben ist.
We couldn't load your accounts right now.
Please try again.
Eine abgelaufene Sitzung verdient eigene Formulierungen:
Your session has expired.
Please sign in again.
Ebenso ein Berechtigungsproblem:
You don't have permission to access this resource.
Eine gute Meldung beantwortet drei Fragen:
- Was ist schiefgelaufen?
- Muss der Benutzer etwas unternehmen?
- Was kann er als Nächstes tun?
Durch die Zuordnung von Statuscodes zu Meldungen an einem Ort bleibt die Formulierung konsistent.
Wiederholen Sie transiente Fehler – aber nur solche, die sicher sind
Einige Fehler sind vorübergehend, wie zum Beispiel ein Netzwerkproblem. Für solche Fälle bietet RxJS die Methode retry().
this.http.get('/api/accounts')
.pipe(
retry(2)
)
.subscribe({
next: (data) => {
this.accounts = data;
},
error: () => {
this.errorMessage =
'Unable to load accounts.';
}
});
retry(2) versucht bis zu zwei weitere Male, sich anzumelden, bevor der Fehler zum error-Callback gelangt. Da die Observables von HttpClient „kalt“ sind, sendet jede erneute Anmeldung eine neue Anfrage.
Darin liegt auch die Gefahr. Wenn eine Zahlanfrage auf dem Server erfolgreich war, aber ihre Antwort verloren ging, kann ein erneuter Versuch den Kunden doppelt belasten – es sei denn, der Backend-Server unterstützt Idempotenz. Seien Sie vorsichtig mit automatischen Wiederholungsversuchen bei:
- Zahlungen
- Bestellungen
Nur-Lese-GET-Anfragen sind viel einfacher zu verstehen. Beachten Sie außerdem, dass eine einfache retry()-Funktion bei jedem Fehler erneut versucht, einschließlich 400- oder 401-Fehlern, die niemals erfolgreich sein werden; in der Regel sollten Sie nur Netzwerkfehler und 5xx-Antworten erneut versuchen.
Wiederholte Anfragen mit Verzögerung
Eine sofortige Wiederholung trifft oft auf denselben überlasteten Server. Das Konfigurationsformular von retry() fügt eine Pause ein.
import { retry } from 'rxjs';
this.http.get('/api/accounts')
.pipe(
retry({
count: 2,
delay: 1000
})
)
.subscribe();
Die Anfrage erhält zwei weitere Chancen, jeweils eine Sekunde voneinander getrennt. delay akzeptiert außerdem eine Funktion, mit der Sie ein exponentielles Zurücksetzen (jede Wartezeit ist länger als die vorherige) implementieren sowie Wiederholungen für Fehler, die nicht erfolgreich sein können, überspringen können. Durch das Zurücksetzen wird verhindert, dass viele fehlerhafte Clients einen sich erholenden Server gleichzeitig belasten.
Abgelaufene Sessions als globales Problem betrachten
Ein klassisches Szenario: Ein Benutzer öffnet die Anwendung am Morgen und kehrt Stunden später zurück. Der Token ist abgelaufen, wodurch die nächste Anfrage fehlschlägt.
GET /api/accounts
401 Unauthorized
Der Interceptor ist der geeignete Ort, um dies zu erkennen:
if (error.status === 401) {
// Clear authentication state
// Redirect to login
}
Durch die Behandlung an einem Ort muss kein Komponente den Ablauf von Sessions neu implementieren. Mit Refresh-Tokens kann der Interceptor vor dem Umleiten eine Erneuerung versuchen; wenn mehrere Anfragen gleichzeitig fehlschlagen, siehe warum konkurrierende 401-Fehler Benutzer ausloggen für den Single-Flight-Ansatz.
Ein Wiederholungsknopf in die Hände des Benutzers legen
Oft ist das wirksamste Wiederherstellungstool ein einfacher Knopf, der die Anfrage erneut sendet.
<div *ngIf="errorMessage">
<p>{{ errorMessage }}</p>
<button (click)="loadAccounts()">
Try Again
</button>
</div>
Stellen Sie sich einen Benutzer vor, dessen Verbindung für einige Sekunden unterbrochen wird. Sobald er wieder online ist, muss er die Anwendung nicht neu laden und verliert keinen Zustand; er drückt einfach:
Try Again
Weil loadAccounts() zunächst seinen Zustand zurücksetzt, dient es sowohl der initialen Ladenung als auch jeder erneuten Anfrage. In Angular 17+ können Templates @if anstelle von *ngIf verwenden.
Auf veraltete Daten zurückgreifen, wenn die Aktualität es zulässt
Falls ein Neuladen des Dashboards fehlschlägt, sind die zuletzt geladenen Werte zusammen mit einer ehrlichen Meldung in der Regel besser als eine leere Seite.
Showing your last available data.
We couldn't refresh the dashboard.
Ein Cache macht dies möglich, und die Meldung ist unerlässlich. Vermeiden Sie das Szenario, in dem die Aktualität entscheidend ist: Bilanzzahlen, Aktienkurse, Lagerbestände und Zahlungsdetails sollten einen Fehler anzeigen statt veralteter Werte.
Aufschlussreiche Logs zum Fehlersuchen erstellen
Das Verbergen von Details vor Benutzern bedeutet nicht, sie zu vernachlässigen. Ein nützliches Protokolleintrag enthält:
Endpoint
HTTP status
Timestamp
Request identifier
User/session context where appropriate
Error category
Eine minimale Interceptor-Version:
console.error({
endpoint: req.url,
status: error.status,
message: error.message
});
In der Produktion beobachtet niemand die Konsole, daher sollte die Nutzlast an einen Fehlerüberwachungsdienst gesendet werden, wo der Anfragen-Identifikator sie mit den Backend-Protokollen abgleicht. Protokollieren Sie niemals Token oder unnötige personenbezogene Daten.
Das Ziel ist ein Gleichgewicht: der Benutzer erhält eine hilfreiche Nachricht, und der Entwickler bekommt genügend Details, um die Ursache zu finden.
Verschleiern Sie Fehler nicht hinter leeren Ergebnissen
Ein verlockender Kurzweg besteht darin, den Fehler zu ignorieren und einen leeren Wert zurückzugeben:
catchError(() => {
return of([]);
})
Es ändert heimlich die Bedeutung: Ein leeres Array suggeriert, der Server habe keine Datensätze zurückgegeben. Ein Benutzer, der keine Transaktionen sieht, könnte daraus schließen, es gäbe überhaupt keine, was schlimmer ist als ein Fehler. Fallback-Lösungen können richtig sein – aber absichtlich; ansonsten muss der Fehler weitergegeben werden:
catchError((error) => {
return throwError(() => error);
})
Trennung der Verantwortlichkeiten zwischen globaler und funktionsbezogener Behandlung
Die globale Behandlung eignet sich für Probleme, die überall gleich sind:
401 → Authentication handling
403 → Authorization handling
500 → Generic server error
Diese gehören in einen Interceptor. Betrachten wir nun:
POST /api/transfer-money
Eine fehlgeschlagene Überweisung erfordert eine spezifische Erklärung sowie eine klare Angabe, ob Geld überwiesen wurde – etwas, was ein generischer Hinweis nicht liefern kann. Die Funktion muss den Fehler erhalten, weshalb der Interceptor ihn erneut wirft. Ein robuster Entwurf besteht aus globaler Behandlung für gemeinsame Probleme plus funktionsbezogener Behandlung, wo der Geschäftskontext wichtig ist.
Überprüfungsliste für Fehlerverläufe
Fehlerbehandlung bedeutet, zu planen, was passiert, wenn etwas ausfällt. Beim Überprüfen einer Funktion sollten Sie Folgendes fragen:
- Versteht der Benutzer, was passiert ist?
- Kann er ohne Neuladen der Seite wieder zum Normalzustand zurückkehren?
- Gibt es eine Möglichkeit, es erneut zu versuchen?
- Hört der Ladeindikator immer auf zu anzeigen?
- Werden technische Details aus der Benutzeroberfläche ferngehalten?
- Wird der Fehler an einem Ort protokolliert, den Entwickler sehen können?
- Was passiert, wenn das Token während der Sitzung abläuft?
- Was passiert, wenn die Verbindung unterbrochen wird?
- Was passiert, wenn der Backend-Daten in einer unerwarteten Form zurückgibt?
Kernpunkte
Fehler sind normal, daher sollte man darauf achten, fehlerfrei zu arbeiten. In Angular wird das durch Folgendes erreicht:
- Eindeutige Lade-, Erfolgs- und Fehlerzustände für jede Anfrage
finalize() sorgt dafür, dass die Aufräumarbeiten unabhängig davon ausgeführt werden, wie der Anfrage abgeschlossen wirdcatchError() dient zur Steuerung von Fehlern, nicht dazu, sie zu verbergenEine zuverlässige Anwendung wird durch ihr Verhalten bei Fehlern definiert.