Startseite / Artikel

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.

Verlag: REACTAPP.TOP9 Min. Lesezeit2160 Wörter

Quellen: **Handling API Failures Gracefully in Angular: Practical Error Handling Patterns Every Frontend Developer Should Know**

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:

  1. Was ist schiefgelaufen?
  2. Muss der Benutzer etwas unternehmen?
  3. 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
  • Kontoinanschaffung
  • Datenaktualisierungen
  • Löschvorgänge
  • 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 wird
  • catchError() dient zur Steuerung von Fehlern, nicht dazu, sie zu verbergen
  • Ein Interceptor für Ablauf der Sitzung, Berechtigungen und allgemeine Serverfehler, der diese erneut auslöst, damit die Funktionen weiter reagieren können
  • Wiederholungsversuche sind auf sichere, idempotente Anfragen beschränkt und erfolgen mit Verzögerungen oder Backoff-Strategien
  • Nachrichten, die den Benutzern mitteilen, was passiert ist und was sie als Nächstes tun sollen
  • Manuelle Wiederholungsknöpfe sowie, sofern die Aktualität es zulässt, klar beschriftete Ersatzdaten
  • Gestrukturiertes Logging, das in der Produktion an einen Überwachungsdienst gesendet wird
  • Eine zuverlässige Anwendung wird durch ihr Verhalten bei Fehlern definiert.

    Verwandte Literatur