← Retour au blog
Angularerror-handlinginterceptorsmonitoringresilience

Global Error Handling in Angular: Building a Robust System with ErrorHandler and Interceptors

An Angular project without a global error handling strategy resembles a system without a safety net: each component manages its own errors, logs scatter everywhere, and user experience becomes unpredictable. Centralized handling eliminates this fragmentation by channeling all errors—HTTP, uncaught exceptions, promise rejections—through a single processing point. This doesn't mean ignoring business-level errors (form validation, user confirmation), but rather building a robust layer that prevents bugs from slipping undetected into production and ensures a consistent experience.

Building a Custom ErrorHandler

The foundation of global error handling rests on implementing a custom ErrorHandler that replaces Angular's default handler. This service intercepts all uncaught exceptions, including synchronous errors and unhandled promise rejections. Rather than simply logging to the console, a robust ErrorHandler classifies errors (network, validation, server), enriches them with context (current URL, user state, app version), and sends them to a centralized monitoring service. The key is distinguishing expected errors (404, timeout) from unexpected ones that reveal a real bug.

Here's a concrete implementation:

import { ErrorHandler, Injectable, Injector } from '@angular/core';

import { HttpErrorResponse } from '@angular/common/http';

import { LoggingService } from './logging.service';

import { NotificationService } from './notification.service';

@Injectable()

export class GlobalErrorHandler implements ErrorHandler {

constructor(private injector: Injector) {}

handleError(error: Error | HttpErrorResponse): void {

const loggingService = this.injector.get(LoggingService);

const notificationService = this.injector.get(NotificationService);

let userMessage = 'An error occurred. Please try again.';

let severity = 'error';

if (error instanceof HttpErrorResponse) {

if (error.status === 0) {

userMessage = 'Cannot reach the server. Check your connection.';

severity = 'warning';

} else if (error.status >= 500) {

userMessage = 'Server error. We are working on it.';

severity = 'error';

} else if (error.status === 401) {

userMessage = 'Session expired. Reconnecting...';

severity = 'info';

}

}

loggingService.logError({

message: error.message,

stack: error instanceof Error ? error.stack : undefined,

context: {

timestamp: new Date().toISOString(),

url: window.location.href,

userAgent: navigator.userAgent

}

});

notificationService.notify(userMessage, severity);

}

}```

Using Injector.get() rather than constructor injection avoids circular dependencies: the logging and notification services shouldn't depend on the ErrorHandler themselves. Error classification allows you to tailor messages to users without exposing sensitive details.

Intercepting and Transforming HTTP Errors

HTTP errors deserve special attention because they're predictable and often recoverable. A dedicated HttpErrorInterceptor captures error responses, applies intelligent retry strategies, and transforms them into domain concepts before they bubble up to components. This reduces boilerplate: components no longer need to handle 401s or temporary timeouts themselves.

import { Injectable } from '@angular/core';

import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';

import { Observable, throwError, timer } from 'rxjs';

import { retry, retryWhen, mergeMap, finalize } from 'rxjs/operators';

@Injectable()

export class HttpErrorInterceptor implements HttpInterceptor {

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {

return next.handle(request).pipe(

retryWhen(errors =>

errors.pipe(

mergeMap((error: HttpErrorResponse, index) => {

if (this.isRetryable(error) && index < 3) {

return timer(Math.pow(2, index) * 1000);

}

return throwError(() => error);

})

)

),

finalize(() => {

// cleanup or logging

})

);

}

private isRetryable(error: HttpErrorResponse): boolean {

return (error.status === 0 || error.status >= 500) && error.status !== 501;

}

}```

This pattern applies exponential backoff (1s, 2s, 4s) and only retries network errors or 5xx responses. 4xx errors (404, 403, 400) are left alone because replaying won't fix them. The finalize() operator lets you clean up resources or send metrics regardless of success or failure.

Rich Context and Traceability

An error without context is hard to debug. An injectable ErrorContext enriches each error with relevant information: current route, user state, app version, request parameters. This context should be collected at the GlobalErrorHandler level and sent to your monitoring system (Sentry, LogRocket, etc.). Avoid logging sensitive data (tokens, passwords, card numbers); use strict sanitization.

interface ErrorContext {

timestamp: string;

url: string;

userId?: string;

routePath: string;

appVersion: string;

requestId?: string;

userAgent: string;

}

loggingService.logError({

error,

context: {

...baseContext,

requestId: request.headers.get('x-request-id'),

userId: currentUser?.id

}

});```

Adding a requestId correlates client-side errors with server logs. If your infrastructure uses OpenTelemetry or distributed tracing, propagate this ID in headers for complete visibility across your stack.

Common Pitfalls and Best Practices

A frequent mistake is logging the same error twice: once in the HTTP interceptor, once in the GlobalErrorHandler. Result: duplicate entries in your monitoring logs and noise that obscures real bugs. Solution: let the interceptor transform and enrich, but make the GlobalErrorHandler the sole caller of loggingService.log(). Another pitfall: not testing error handling in staging. Network errors, timeouts, and promise rejections are rare in local development. Use tools like Cypress or Playwright to simulate slow responses (throttling) and partial network outages. Finally, beware of overly broad try-catch blocks that mask errors silently: catching Error and continuing transforms a detectable bug into invisible erratic behavior.

Conclusion

Robust global error handling in Angular rests on three pillars: a GlobalErrorHandler that centralizes logic, an HttpErrorInterceptor that handles network cases with intelligent retry, and rich context that enables debugging. This approach eliminates fragmentation, reduces boilerplate, and improves resilience. Start by implementing the GlobalErrorHandler and HTTP interceptor, then progressively enrich context based on your monitoring needs. Test aggressively in staging with degraded network conditions. A well-designed error system won't make your app infallible, but it lets you detect and fix real problems quickly, rather than discovering obscure bugs in production.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️