Angular · Architecture

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.

Advanced 7 min Lesson 32 of 38Examples target Angular 20 with standalone components. Each lesson states the version it was verified against.
Before you start:HttpClient
0/38 in Angular
On this page · 7 min · Advanced

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#

TypeScript Illustrative
@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#

TypeScripterrors.ts Illustrative
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 }]
Expected output
“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#

  1. ToastService keeps messages in a signal and removes them after 5 seconds.
  2. toUserMessage maps status codes (and ProblemDetails titles) to plain language.
  3. AppErrorHandler replaces Angular’s default handler for uncaught errors.
  4. retry only waits and retries for 5xx and network errors; a 404 fails immediately.
  5. 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#

Showing raw errors

“Http failure response for https://…: 500 Internal Server Error” confuses users. Map to friendly messages.

Retrying non-idempotent requests

Retrying a failed “Place order” POST can create duplicate orders. Retry only safe operations.

Catching and ignoring

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?
Add a case err.status === 409 before the 5xx case.
Show solution
TypeScript Illustrative
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#

Error Handling quizMultiple choice Question 1 of 3

What does an HttpErrorResponse with status 0 usually mean?

Score so far: 0/0

Want more? Practise all Angular MCQs · Angular interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.