O strategie bună de erori are straturi: erorile previzibile le tratezi local (un mesaj lângă date), cele transversale centralizat (interceptor), iar tot ce scapă ajunge într-un ErrorHandler global, care le loghează. Nou în v22.2: blocul @boundary izolează erorile de randare ale unei zone, ca o eroare într-un widget să nu strice toată pagina.
Straturile
Unde tratezi fiecare tip de eroare:
Eroare HTTP previzibilă (404, 422) → în store/componentă: resource.error(), mesaj pentru utilizator
Eroare HTTP transversală (401, 500) → interceptor (redirect la login, toast, retry)
Eroare de randare a unei zone → @boundary (v22.2, developer preview)
Orice altceva ne-prins → ErrorHandler global (log + telemetrie)
Validare de formular → Signal Forms (errors(), nu excepții)@boundaryv22 developer preview
<!-- v22.2 (developer preview): izolează erorile de randare ale unei zone -->
@boundary {
<app-price-widget [price]="price()" />
<app-recommendations />
} @error (let error = $error; let reset = $reset) {
<app-error-card [message]="error.message" (retry)="reset()" />
}
<!-- Mai multe @error cu condiții `when` — primul care se potrivește câștigă -->
@boundary {
<app-report />
} @error (let e = $error; when e instanceof PermissionError) {
<p>Nu ai acces la acest raport.</p>
} @error (let e = $error; let retry = $reset) {
<p>Ceva n-a mers. <button (click)="retry()">Reîncearcă</button></p>
}@boundary și API-urile ErrorBoundary (ErrorDetails, onError la createComponent) sunt marcate @developerPreview 22.2. Poți să le folosești, dar sintaxa se mai poate schimba. Eroarea prinsă e raportată în continuare și la 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, backend propriu
console.error(error);
}
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(), // window.onerror + unhandledrejection → ErrorHandler
{ provide: ErrorHandler, useClass: AppErrorHandler },
],
};- Nicio eroare încă. Apasă un buton — eroarea ajunge la AppErrorHandler, înregistrat în app.config.ts.
Erori HTTP
// Interceptor pentru erori transversale
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('Serverul are probleme. Încearcă mai târziu.', 'bad');
return throwError(() => err); // lasă și apelantul să reacționeze
}),
);
};Componente dinamice: onError
// Pentru componente create dinamic (v22.2): onError
const ref = viewContainer.createComponent(Widget, {
onError: (error, details) => {
console.warn('Widget-ul', details.declarationType.name, 'a eșuat', error);
this.vcr.clear(); // înlocuiește cu un fallback
},
});Erori din resource și httpResource
O resursă nu aruncă eroarea mai departe. O ține în starea ei: status() devine 'error', iar error() spune ce s-a întâmplat. Atenție: dacă citești value() în starea de eroare, aceea aruncă. De aceea template-ul verifică întâi hasValue() sau error().
readonly user = httpResource<User>(() => `/api/users/${this.id()}`);
// întâi eroarea, apoi valoarea: value() aruncă în starea de eroare
@if (user.error(); as e) {
<p role="alert">Nu am putut încărca profilul.</p>
<button (click)="user.reload()">Încearcă din nou</button>
} @else if (user.hasValue()) {
<h2>{{ user.value().name }}</h2>
} @else {
<app-skeleton />
}Erorile care nu trec prin Angular
Unele erori nu pornesc din cod rulat de Angular: un setTimeout dintr-o librărie, un Promise respins fără catch. provideBrowserGlobalErrorListeners() ascultă evenimentele error și unhandledrejection ale ferestrei și le trimite tot la ErrorHandler, ca să ai un singur loc pentru toate. Proiectele noi îl primesc deja în app.config.ts.
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(), // error + unhandledrejection din window → ErrorHandler
{ provide: ErrorHandler, useClass: AppErrorHandler },
],
};Ce trimiți la server
Un ErrorHandler care doar scrie în consolă nu te ajută în producție, pentru că nimeni nu vede consola utilizatorului. Trimite erorile la un serviciu de logging, dar cu grijă:
- Deduplică. Aceeași eroare într-o buclă poate genera mii de rapoarte într-un minut. Ține minte mesajele trimise recent.
- Fără date personale. Nu trimite token-uri, conținutul formularelor sau URL-uri care conțin email-uri. Trimite mesajul, stack-ul și ruta.
- Nu bloca aplicația. Trimite cu
navigator.sendBeaconsau cu un request care nu poate arunca la rândul lui.
Reîncercările cu pauză crescătoare sunt la RxJS avansat: operatori de ordin superior; o pagină care trebuie să răspundă cu 404 la randarea pe server, la Cod compatibil cu serverul.
Capcane frecvente
- Erori înghițite: un
catchError(() => of([]))fără log face bug-urile invizibile. - Toast pentru fiecare eroare: utilizatorul primește 5 mesaje pentru o singură problemă. Centralizează și deduplică.
- ErrorHandler care aruncă: dacă handler-ul tău are un bug, pierzi toate erorile. Ține-l simplu și sigur.
- @boundary nu prinde erori din handler-e de evenimente sau din cod async — doar din randare. Acelea ajung la
ErrorHandler.
Fișă rapidă
// randare: izolează o zonă (v22.2, developer preview)
@boundary { <app-widget /> } @error (let e = $error) { <p>Widget indisponibil</p> }
// local: lângă datele care au eșuat
catchError((e) => { this.error.set(e); return of(null); })
// resurse: eroarea e stare, nu excepție
res.error() res.hasValue() res.status() === 'error' res.reload()
// global: tot ce scapă ajunge aici
provideBrowserGlobalErrorListeners()
{ provide: ErrorHandler, useClass: AppErrorHandler }