Angular Error Handling: Global ErrorHandler, Custom Handlers and HTTP Errors

Angular Tutorial: Beginner to Advanced · 15. Architecture, configuration and errors · 4 min read · Updated

Every app breaks sometimes. Good error handling means users see a helpful message, and you get the details to fix the bug.

What you will learn

  • The global ErrorHandler and how to replace it
  • Handling HttpErrorResponse in one place with an interceptor
  • Showing friendly messages and logging to your server

Two kinds of errors

  1. Unexpected bugs in your code (undefined is not a function). Use a global handler to log them.
  2. Expected failures such as a 404, a lost connection or a validation error from the server. Handle them where they happen, and show a message.

The global ErrorHandler

Angular sends every uncaught error to a service called ErrorHandler. By default it prints to the console. Replace it to send errors to your own logging:

// src/app/errors/error-log.ts
import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class ErrorLog {
  readonly messages = signal<string[]>([]);
  add(message: string) {
    this.messages.update((list) => [...list, message]);
  }
}
// src/app/errors/global-error-handler.ts
import { ErrorHandler, Injectable, inject } from '@angular/core';
import { ErrorLog } from './error-log';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
  private readonly log = inject(ErrorLog);

  handleError(error: unknown): void {
    const message = error instanceof Error ? error.message : String(error);
    this.log.add(message);
    console.error('Unhandled error:', error); // keep the console output for developers
    // in a real app: fetch('/api/log', { method: 'POST', body: JSON.stringify({ message }) })
  }
}

Register it in app.config.ts:

// File: app.config.ts
providers: [
  { provide: ErrorHandler, useClass: GlobalErrorHandler },
  provideBrowserGlobalErrorListeners(), // also catches errors outside Angular, like window.onerror
]

Logic: you replace one provider, and from then on every unhandled error flows through your code. The provideBrowserGlobalErrorListeners() function (already in new projects) forwards window errors and unhandled promise rejections to the same handler.

HTTP errors in one place

A failed request gives an HttpErrorResponse. Its status tells you what happened: 0 means no network, 401 not signed in, 404 not found, 500 server bug. Translate them into messages in one interceptor:

// src/app/errors/http-error.interceptor.ts
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { ErrorLog } from './error-log';

export function friendlyMessage(error: HttpErrorResponse): string {
  switch (error.status) {
    case 0:
      return 'No connection. Check your internet and try again.';
    case 401:
      return 'Please sign in again.';
    case 404:
      return 'We could not find that.';
    default:
      return error.status >= 500 ? 'The server had a problem. Please try later.' : 'Something went wrong.';
  }
}

export const httpErrorInterceptor: HttpInterceptorFn = (req, next) => {
  const log = inject(ErrorLog);
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      log.add(friendlyMessage(error));
      return throwError(() => error); // rethrow so the caller can still react
    }),
  );
};
// File: register it
provideHttpClient(withInterceptors([httpErrorInterceptor]))

Notice: the interceptor rethrows. The component can still show a retry button or stop its spinner, while the interceptor shows the message centrally.

Showing the messages

// src/app/errors/error-banner.ts
import { Component, inject } from '@angular/core';
import { ErrorLog } from './error-log';

@Component({
  selector: 'app-error-banner',
  template: `
    @for (m of log.messages(); track $index) {
      <p role="alert" class="error">{{ m }}</p>
    }
  `,
})
export class ErrorBanner {
  protected readonly log = inject(ErrorLog);
}

role="alert" makes screen readers announce the message.

Test it

// src/app/errors/errors.spec.ts
import { ErrorHandler } from '@angular/core';
import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { ErrorLog } from './error-log';
import { GlobalErrorHandler } from './global-error-handler';
import { httpErrorInterceptor } from './http-error.interceptor';

describe('error handling', () => {
  it('global handler records unexpected errors', () => {
    TestBed.configureTestingModule({ providers: [{ provide: ErrorHandler, useClass: GlobalErrorHandler }] });
    vi.spyOn(console, 'error').mockImplementation(() => {});
    TestBed.inject(ErrorHandler).handleError(new Error('boom'));
    expect(TestBed.inject(ErrorLog).messages()).toEqual(['boom']);
  });

  it('interceptor turns a 404 into a friendly message and rethrows', () => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(withInterceptors([httpErrorInterceptor])), provideHttpClientTesting()],
    });
    let failed = false;
    TestBed.inject(HttpClient).get('/api/x').subscribe({ error: () => (failed = true) });
    TestBed.inject(HttpTestingController).expectOne('/api/x').flush('nope', { status: 404, statusText: 'Not Found' });
    expect(failed).toBe(true);
    expect(TestBed.inject(ErrorLog).messages()).toEqual(['We could not find that.']);
  });
});

Common mistakes

  • Swallowing errors with an empty catch. You lose the bug report.
  • Showing raw server messages or stack traces to users.
  • Logging personal data (passwords, tokens) in error reports.
  • Handling the same error in the interceptor and showing a second message in the component.

Try it yourself

  1. Throw an error from a button click and see your handler log it.
  2. Add a 403 case with the message "You do not have permission".
  3. Send the message to your backend with fetch and store it.

Quick check

  1. Does a catchError handled error reach ErrorHandler?
  2. What does HTTP status 0 mean?
  3. Why rethrow inside the interceptor?

Next, continue with Angular NgModules (Legacy), Module Routing, Lazy Modules and Migration to Standalone.

Frequently asked questions

Where should I send production errors?

To a logging service or your own endpoint, from your custom ErrorHandler. Services such as Sentry plug in the same way.

Does ErrorHandler catch errors in Observables?

An error with no handler in subscribe is rethrown asynchronously, and the global error listener passes it on. Do not rely on that for expected failures: handle them with catchError or an error callback.