Eine gute Fehlerstrategie hat Schichten: Vorhersehbare Fehler behandelst du lokal (eine Meldung neben den Daten), übergreifende zentral (ein Interceptor), und alles, was durchrutscht, landet in einem globalen ErrorHandler, der es protokolliert. Neu in v22.2: Der @boundary-Block isoliert die Rendering-Fehler eines Bereichs, sodass ein Fehler in einem Widget nicht die ganze Seite kaputt macht.
Die Schichten
Wo du welche Art von Fehler behandelst:
Vorhersehbarer HTTP-Fehler (404, 422) → im Store/in der Komponente: resource.error(), eine Meldung für die Nutzer
Übergreifender HTTP-Fehler (401, 500) → Interceptor (Weiterleitung zum Login, Toast, Retry)
Rendering-Fehler eines Bereichs → @boundary (v22.2, Developer Preview)
Alles andere, was nicht abgefangen wird → globaler ErrorHandler (Log + Telemetrie)
Formularvalidierung → Signal Forms (errors(), keine Exceptions)@boundaryv22 Developer Preview
<!-- v22.2 (developer preview): isoliert Rendering-Fehler eines Bereichs -->
@boundary {
<app-price-widget [price]="price()" />
<app-recommendations />
} @error (let error = $error; let reset = $reset) {
<app-error-card [message]="error.message" (retry)="reset()" />
}
<!-- Mehrere @error-Blöcke mit `when`-Bedingungen — der erste Treffer gewinnt -->
@boundary {
<app-report />
} @error (let e = $error; when e instanceof PermissionError) {
<p>Du hast keinen Zugriff auf diesen Bericht.</p>
} @error (let e = $error; let retry = $reset) {
<p>Etwas ist schiefgelaufen. <button (click)="retry()">Erneut versuchen</button></p>
}@boundary und die ErrorBoundary-APIs (ErrorDetails, onError bei createComponent) sind mit @developerPreview 22.2 markiert. Du kannst sie nutzen, aber die Syntax kann sich noch ändern. Ein abgefangener Fehler wird weiterhin an ErrorHandler gemeldet.
Globaler ErrorHandler
import { ErrorHandler, inject } from '@angular/core';
export class AppErrorHandler implements ErrorHandler {
private readonly log = inject(ErrorLog);
private readonly telemetry = inject(Telemetry);
handleError(error: unknown): void {
this.log.add(error);
this.telemetry.captureException(error); // Sentry, App Insights, dein eigenes Backend
console.error(error);
}
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(), // window.onerror + unhandledrejection → ErrorHandler
{ provide: ErrorHandler, useClass: AppErrorHandler },
],
};- Noch kein Fehler. Drücke einen Button — der Fehler erreicht AppErrorHandler, der in app.config.ts registriert ist.
HTTP-Fehler
// Interceptor für übergreifende Fehler
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const toasts = inject(ToastService);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) router.navigate(['/login']);
else if (err.status >= 500) toasts.show('Der Server hat Probleme. Versuche es später erneut.', 'bad');
return throwError(() => err); // den Aufrufer auch reagieren lassen
}),
);
};Dynamische Komponenten: onError
// Für dynamisch erzeugte Komponenten (v22.2): onError
const ref = viewContainer.createComponent(Widget, {
onError: (error, details) => {
console.warn('Das Widget', details.declarationType.name, 'ist fehlgeschlagen', error);
this.vcr.clear(); // durch einen Fallback ersetzen
},
});Fehler aus resource und httpResource
Eine Resource wirft den Fehler nicht weiter. Sie hält ihn in ihrem Zustand: status() wird 'error', und error() sagt, was passiert ist. Achtung: value() im Fehlerzustand zu lesen wirft. Deshalb prüft das Template zuerst hasValue() oder error().
readonly user = httpResource<User>(() => `/api/users/${this.id()}`);
// erst der Fehler, dann der Wert: value() wirft im Fehlerzustand
@if (user.error(); as e) {
<p role="alert">Das Profil konnte nicht geladen werden.</p>
<button (click)="user.reload()">Erneut versuchen</button>
} @else if (user.hasValue()) {
<h2>{{ user.value().name }}</h2>
} @else {
<app-skeleton />
}Fehler, die nicht durch Angular laufen
Manche Fehler entstehen nicht in Code, den Angular ausführt: ein setTimeout in einer Bibliothek, ein Promise, das ohne catch abgelehnt wird. provideBrowserGlobalErrorListeners() hört auf die Window-Events error und unhandledrejection und schickt sie ebenfalls an den ErrorHandler, damit du einen Ort für alle hast. Neue Projekte haben es bereits in app.config.ts.
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(), // error + unhandledrejection aus window → ErrorHandler
{ provide: ErrorHandler, useClass: AppErrorHandler },
],
};Was du an den Server schickst
Ein ErrorHandler, der nur in die Konsole schreibt, hilft in Produktion nicht, denn niemand sieht die Konsole des Nutzers. Schick Fehler an einen Logging-Dienst, aber mit Bedacht:
- Deduplizieren. Derselbe Fehler in einer Schleife kann tausende Berichte pro Minute erzeugen. Merke dir kürzlich gesendete Meldungen.
- Keine personenbezogenen Daten. Keine Tokens, Formularinhalte oder URLs mit E-Mail-Adressen senden. Schick Meldung, Stack und Route.
- Die App nicht blockieren. Sende mit
navigator.sendBeaconoder mit einem Request, der selbst nicht werfen kann.
Wiederholungen mit wachsender Pause zeigt Fortgeschrittenes RxJS: Operatoren höherer Ordnung; eine Seite, die beim Server-Rendering mit 404 antworten muss, Servertauglicher Code.
Häufige Stolperfallen
- Verschluckte Fehler: Ein
catchError(() => of([]))ohne Logging macht Bugs unsichtbar. - Ein Toast für jeden Fehler: Die Nutzer bekommen 5 Meldungen für ein einziges Problem. Zentralisiere und dedupliziere.
- Ein ErrorHandler, der selbst wirft: Hat dein Handler einen Bug, verlierst du alle Fehler. Halte ihn einfach und sicher.
- @boundary fängt keine Fehler aus Event-Handlern oder aus asynchronem Code ab — nur aus dem Rendering. Diese erreichen
ErrorHandler.
Spickzettel
// Rendering: einen Bereich isolieren (v22.2, Developer Preview)
@boundary { <app-widget /> } @error (let e = $error) { <p>Widget nicht verfügbar</p> }
// lokal: neben den Daten, die fehlgeschlagen sind
catchError((e) => { this.error.set(e); return of(null); })
// Resources: der Fehler ist Zustand, keine Exception
res.error() res.hasValue() res.status() === 'error' res.reload()
// global: alles, was durchrutscht, landet hier
provideBrowserGlobalErrorListeners()
{ provide: ErrorHandler, useClass: AppErrorHandler }