Une bonne stratégie d’erreurs a des couches : les erreurs prévisibles sont gérées localement (un message à côté des données), les erreurs transversales de façon centralisée (un interceptor), et tout ce qui s’échappe arrive dans un ErrorHandler global, qui le journalise. Nouveau en v22.2 : le bloc @boundary isole les erreurs de rendu d’une zone, de sorte qu’une erreur dans un widget ne casse pas toute la page.
Les couches
Où traiter chaque type d’erreur :
Erreur HTTP prévisible (404, 422) → dans le store/composant : resource.error(), un message pour l’utilisateur
Erreur HTTP transversale (401, 500) → interceptor (redirection vers le login, toast, retry)
Erreur de rendu d’une zone → @boundary (v22.2, aperçu développeur)
Tout le reste, non intercepté → ErrorHandler global (log + télémétrie)
Validation de formulaire → Signal Forms (errors(), pas des exceptions)@boundaryv22 aperçu développeur
<!-- v22.2 (aperçu développeur) : isole les erreurs de rendu d’une zone -->
@boundary {
<app-price-widget [price]="price()" />
<app-recommendations />
} @error (let error = $error; let reset = $reset) {
<app-error-card [message]="error.message" (retry)="reset()" />
}
<!-- Plusieurs blocs @error avec des conditions `when` — le premier qui correspond l’emporte -->
@boundary {
<app-report />
} @error (let e = $error; when e instanceof PermissionError) {
<p>Vous n’avez pas accès à ce rapport.</p>
} @error (let e = $error; let retry = $reset) {
<p>Une erreur est survenue. <button (click)="retry()">Réessayer</button></p>
}@boundary et les API ErrorBoundary (ErrorDetails, onError sur createComponent) sont marqués @developerPreview 22.2. Vous pouvez les utiliser, mais la syntaxe peut encore changer. Une erreur interceptée est tout de même signalée à ErrorHandler.
ErrorHandler global
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, votre propre backend
console.error(error);
}
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(), // window.onerror + unhandledrejection → ErrorHandler
{ provide: ErrorHandler, useClass: AppErrorHandler },
],
};- Aucune erreur pour l’instant. Appuyez sur un bouton — l’erreur arrive dans AppErrorHandler, enregistré dans app.config.ts.
Erreurs HTTP
// Interceptor pour les erreurs transversales
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('Le serveur rencontre des problèmes. Réessayez plus tard.', 'bad');
return throwError(() => err); // laisser aussi l’appelant réagir
}),
);
};Composants dynamiques : onError
// Pour les composants créés dynamiquement (v22.2) : onError
const ref = viewContainer.createComponent(Widget, {
onError: (error, details) => {
console.warn('Le widget', details.declarationType.name, 'a échoué', error);
this.vcr.clear(); // remplacer par un fallback
},
});Erreurs de resource et httpResource
Une resource ne relance pas l’erreur. Elle la garde dans son état : status() devient 'error', et error() dit ce qui s’est passé. Attention : lire value() en état d’erreur lève une exception. C’est pourquoi le template vérifie d’abord hasValue() ou error().
readonly user = httpResource<User>(() => `/api/users/${this.id()}`);
// d’abord l’erreur, puis la valeur : value() lève une exception en état d’erreur
@if (user.error(); as e) {
<p role="alert">Impossible de charger le profil.</p>
<button (click)="user.reload()">Réessayer</button>
} @else if (user.hasValue()) {
<h2>{{ user.value().name }}</h2>
} @else {
<app-skeleton />
}Les erreurs qui ne passent pas par Angular
Certaines erreurs ne naissent pas dans du code exécuté par Angular : un setTimeout dans une bibliothèque, une Promise rejetée sans catch. provideBrowserGlobalErrorListeners() écoute les événements error et unhandledrejection de la fenêtre et les envoie aussi à l’ErrorHandler, pour un seul endroit qui les reçoit toutes. Les nouveaux projets l’ont déjà dans app.config.ts.
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(), // error + unhandledrejection de window → ErrorHandler
{ provide: ErrorHandler, useClass: AppErrorHandler },
],
};Ce que vous envoyez au serveur
Un ErrorHandler qui écrit seulement dans la console n’aide pas en production : personne ne voit la console de l’utilisateur. Envoyez les erreurs à un service de logging, mais avec soin :
- Dédupliquez. La même erreur dans une boucle peut produire des milliers de rapports par minute. Gardez en mémoire les messages envoyés récemment.
- Pas de données personnelles. N’envoyez ni tokens, ni contenu de formulaires, ni URL contenant des e-mails. Envoyez le message, la pile et la route.
- Ne bloquez pas l’application. Envoyez avec
navigator.sendBeaconou avec une requête qui ne peut pas lever d’erreur à son tour.
Les nouvelles tentatives avec délai croissant sont dans RxJS avancé : opérateurs d’ordre supérieur ; une page qui doit répondre 404 au rendu serveur, dans Code compatible serveur.
Pièges fréquents
- Des erreurs avalées : un
catchError(() => of([]))sans journalisation rend les bugs invisibles. - Un toast pour chaque erreur : l’utilisateur reçoit 5 messages pour un seul problème. Centralisez et dédupliquez.
- Un ErrorHandler qui lance une erreur : si votre handler a un bug, vous perdez toutes les erreurs. Gardez-le simple et sûr.
- @boundary n’intercepte pas les erreurs des gestionnaires d’événements ni du code asynchrone — seulement celles du rendu. Celles-ci arrivent dans
ErrorHandler.
Aide-mémoire
// rendu : isoler une zone (v22.2, developer preview)
@boundary { <app-widget /> } @error (let e = $error) { <p>Widget indisponible</p> }
// local : à côté des données en échec
catchError((e) => { this.error.set(e); return of(null); })
// resources : l’erreur est un état, pas une exception
res.error() res.hasValue() res.status() === 'error' res.reload()
// global : tout ce qui échappe arrive ici
provideBrowserGlobalErrorListeners()
{ provide: ErrorHandler, useClass: AppErrorHandler }