Error Handling
Errors will happen — offline users, server bugs, unexpected data. Good Angular apps catch them in a few central places, show users something helpful, and send the details to the developers.
On this page · 7 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Replace Angular’s ErrorHandler for uncaught errors
- Translate HttpErrorResponse into friendly messages
- Show messages with a toast service
- Retry transient failures safely
Explanation#
Uncaught errors (bugs in components, effects, event handlers) go to Angular’s ErrorHandler. Provide your own class to log them and show a generic message. provideBrowserGlobalErrorListeners() also routes window errors and unhandled promise rejections there.
HTTP errors arrive as HttpErrorResponse: status 0 means network/CORS problems, 400 validation (often ProblemDetails with errors), 401/403 auth, 404 not found, 5xx server problems.
Map those to messages in one helper, and show them through a ToastService (signals + a small toast component in the shell).
Retry only safe, transient failures: retry({ count: 2, delay: 1000 }) for GETs that hit 503 or network errors. Never blindly retry POSTs that create orders.
Log technical details (status, URL, correlation id) to your monitoring service; users see plain language.
Syntax#
@Injectable()
export class AppErrorHandler implements ErrorHandler {
handleError(error: unknown) { /* log + toast */ }
}
providers: [{ provide: ErrorHandler, useClass: AppErrorHandler }]
Code example: A global error handler, message mapping and toasts#
import { Component, ErrorHandler, Injectable, inject, signal } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { retry, timer } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ToastService {
readonly messages = signal<{ id: number; text: string }[]>([]);
private next = 1;
show(text: string) {
const id = this.next++;
this.messages.update(m => [...m, { id, text }]);
setTimeout(() => this.dismiss(id), 5000);
}
dismiss(id: number) { this.messages.update(m => m.filter(x => x.id !== id)); }
}
export function toUserMessage(err: unknown): string {
if (err instanceof HttpErrorResponse) {
switch (true) {
case err.status === 0: return 'You seem to be offline. Check your connection.';
case err.status === 400: return err.error?.title ?? 'Some details are not valid.';
case err.status === 401: return 'Please sign in again.';
case err.status === 403: return 'You do not have permission to do that.';
case err.status === 404: return 'We could not find what you were looking for.';
case err.status >= 500: return 'Something went wrong on our side. Please try again.';
}
}
return 'Unexpected error. Please try again.';
}
@Injectable()
export class AppErrorHandler implements ErrorHandler {
private toasts = inject(ToastService);
handleError(error: unknown): void {
console.error('[AppErrorHandler]', error); // send to Sentry/App Insights here
this.toasts.show(toUserMessage(error));
}
}
@Component({
selector: 'app-toasts',
template: `
<div aria-live="polite">
@for (t of toasts.messages(); track t.id) {
<p class="toast">{{ t.text }} <button (click)="toasts.dismiss(t.id)" aria-label="Dismiss">✕</button></p>
}
</div>
`,
styles: `.toast { background: #fee2e2; padding: 6px 10px; border-radius: 6px; }`,
})
export class Toasts {
protected toasts = inject(ToastService);
}
@Component({
selector: 'app-error-demo',
imports: [Toasts],
template: `
<button (click)="loadMissing()">Load missing item</button>
<button (click)="crash()">Trigger a bug</button>
<app-toasts />
`,
})
export class ErrorDemo {
private http = inject(HttpClient);
private toasts = inject(ToastService);
loadMissing() {
this.http.get('https://jsonplaceholder.typicode.com/posts/999999')
.pipe(retry({ count: 2, delay: (err: HttpErrorResponse) => (err.status >= 500 || err.status === 0 ? timer(1000) : Promise.reject(err)) }))
.subscribe({ error: err => this.toasts.show(toUserMessage(err)) });
}
crash() {
throw new Error('Oops: undefined is not a function'); // goes to AppErrorHandler
}
}
// app.config.ts:
// providers: [provideBrowserGlobalErrorListeners(), provideHttpClient(), { provide: ErrorHandler, useClass: AppErrorHandler }]
“Load missing item” gets a 404 (not retried, because it is not transient) and shows the toast “We could not find what you were looking for.”. “Trigger a bug” throws; AppErrorHandler logs the stack trace to the console and shows “Unexpected error. Please try again.”. Toasts disappear after 5 seconds or when dismissed.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- ToastService keeps messages in a signal and removes them after 5 seconds.
- toUserMessage maps status codes (and ProblemDetails titles) to plain language.
- AppErrorHandler replaces Angular’s default handler for uncaught errors.
- retry only waits and retries for 5xx and network errors; a 404 fails immediately.
- The Toasts component announces messages through an aria-live region.
Real-world use case#
A logistics dashboard sends every uncaught error with the user id and route to Application Insights, shows friendly toasts, and silently retries flaky GETs to a legacy tracking API.
Common mistakes#
“Http failure response for https://…: 500 Internal Server Error” confuses users. Map to friendly messages.
Retrying a failed “Place order” POST can create duplicate orders. Retry only safe operations.
catchError(() => EMPTY) everywhere hides real problems. Log or rethrow.
Best practices#
- Custom ErrorHandler + global listeners
- One function that maps errors to messages
- Toasts via a signal-based service
- Retry only transient failures of safe requests
Remember this#
Uncaught → ErrorHandler. HTTP → HttpErrorResponse.status.
Users get plain language; logs get the details.
Retry GETs on 5xx/offline, not POSTs.
Practice exercise#
Your task: Extend toUserMessage so a 409 Conflict shows "This item was changed by someone else. Reload and try again."
Need a hint?
Show solution
import { HttpErrorResponse } from '@angular/common/http';
export function conflictAwareMessage(err: unknown): string {
if (err instanceof HttpErrorResponse && err.status === 409) {
return 'This item was changed by someone else. Reload and try again.';
}
return 'Unexpected error. Please try again.';
}
Mini quiz#
What does an HttpErrorResponse with status 0 usually mean?
Want more? Practise all Angular MCQs · Angular interview questions
My notes#
Saved in this browser.