Error Handling
|
This section documents modern, standalone Angular — signals, the built-in This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted. This section’s bibliography lists the reference material consulted while preparing these pages. |
Angular separates framework errors — raised by Angular itself and carrying a numeric code — from your own runtime errors, and gives you a single global hook to catch, present, and report both. See Error handling.
Framework errors vs. runtime errors
-
Framework errors are thrown by Angular and carry an
NG0000-style code — for exampleNG0100(ExpressionChangedAfterItHasBeenChecked),NG0200(circular dependency in DI), andNG0103(infinite change-detection loop). Every code has a dedicated explanation page in the error reference. -
Runtime errors are the ordinary exceptions your code and its dependencies throw — a
TypeError, a rejected promise, a failed HTTP call.
Both, when left unhandled, reach Angular’s ErrorHandler.
A custom global ErrorHandler
ErrorHandler is the single sink for errors that escape change detection and lifecycle hooks. Provide your own
to normalise, log, de-duplicate, and forward them.
import { ErrorHandler, Injectable, NgZone, inject } from '@angular/core';
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
private readonly zone = inject(NgZone);
private readonly reporter = inject(ErrorReporter);
handleError(error: unknown): void {
const err = error instanceof Error ? error : new Error(String(error));
this.reporter.report(err); // send to the backend
this.zone.run(() => this.notify(err)); // show UI inside Angular's zone
console.error(err);
}
private notify(err: Error): void {
/* trigger a toast/snackbar -- see below */
}
}
// app.config.ts
import {
ApplicationConfig, ErrorHandler, provideBrowserGlobalErrorListeners,
} from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
],
};
provideBrowserGlobalErrorListeners() connects the browser’s error and unhandledrejection events to the
same ErrorHandler, so failures outside Angular’s execution context — event callbacks, un-awaited promises — are caught too. New workspaces include it in the generated app.config.ts. See
ErrorHandler.
Catching HTTP errors centrally
A functional interceptor is the right place to handle transport failures once for the whole app. catchError
inspects the HttpErrorResponse, reacts to the status, and re-throws so the calling code still observes the
error.
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) {
// not authenticated -- go to login, keep the target URL
router.navigate(['/login'], { queryParams: { returnUrl: router.url } });
} else if (err.status === 403) {
// authenticated but not allowed -- dedicated page
router.navigate(['/forbidden']);
}
return throwError(() => err);
}),
);
};
Register it with provideHttpClient(withInterceptors([errorInterceptor])). A 401 means "not authenticated" — redirect to the login page and preserve the target URL so the user lands back where they started; a 403 means
"authenticated but not authorised" — route to a distinct page rather than the login form. See
HTTP Client for the interceptor chain, and
Routing for functional guards that block the navigation before the request is
ever made. Link Interceptors.
Surfacing and reporting errors
To the user: show a non-blocking toast/snackbar with a recoverable message — never a stack trace. With Angular Material:
import { inject } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
private readonly snackbar = inject(MatSnackBar);
private notify(err: Error): void {
this.snackbar.open('Something went wrong. Please try again.', 'Dismiss', { duration: 5000 });
}
To a backend: POST the error (message, stack, URL, user agent, release version) to your logging endpoint or
a service such as Sentry. Send it with a plain fetch rather than HttpClient, so a failing report cannot
re-enter the interceptor above and loop.
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ErrorReporter {
report(err: Error): void {
fetch('/api/client-errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: err.message, stack: err.stack, url: location.href }),
keepalive: true,
}).catch(() => {
/* reporting is best-effort -- never throw from here */
});
}
}
Keep reporting best-effort and side-effect-free on failure. See Error handling for the full guidance, and Testing for asserting that the handler runs.