skip to content

In Angular, which errors reach the ErrorHandler automatically, and which ones never get there?

level: middleimportance: must knowfreq 50%

answer

  1. code Angular calls for you
  2. code you call yourself
  3. explicit contract to await a result
  4. errors shown as state, not thrown
  5. global listeners fill the async gap

basics

~20 s

Errors thrown in code Angular runs for you — constructors, lifecycle hooks, template bindings, template event listeners, async pipe sources, bootstrap — reach ErrorHandler. Errors you catch, errors exposed as state like resource().error, and free async errors without global listeners do not.

solid answer

~40 s

Angular forwards an error to `ErrorHandler` when **it** ran the code and there was nowhere for you to put a `try`: component constructors and lifecycle hooks, template expressions during change detection, template event listeners, a failing source in the `async` pipe (reported directly since v20), `PendingTasks.run`, and bootstrap failures such as a rejected initializer. It does **not** see errors you catch with `try`/`catch` or `catchError`; errors an API presents as state, such as `resource()`'s `error` and `status`; or errors from code you started yourself asynchronously — a `setTimeout` callback, an un-awaited promise, a `subscribe` without an error callback — unless something forwards them. With zone.js, the zone forwards errors from async tasks inside the Angular zone; without it, `provideBrowserGlobalErrorListeners()` forwards window `error` and `unhandledrejection` events.

code

ts · 22 lines
ts
import { Component, resource } from '@angular/core';

@Component({
  selector: 'app-report-list',
  template: `
    <button (click)="exportCsv()">Export</button>
    @if (reports.error()) {
      <p>Could not load reports.</p>
    }
  `,
})
export class ReportList {
  // Failure is exposed as state: never reaches ErrorHandler
  readonly reports = resource({
    loader: () => fetch('/api/reports').then((r) => r.json()),
  });

  exportCsv(): void {
    // Thrown in a template listener: Angular reports it to ErrorHandler
    throw new Error('CSV export failed');
  }
}

go deeper

for a junior

Recall the main sources that reach the handler: hooks, template bindings, template listeners and the async pipe.

for a middle

Explain the rule of who called the code, and why APIs that expose errors as state never forward them.

for a senior

Audit monitoring coverage: which async paths depend on zone.js or the global listeners, and what a zoneless migration silently drops.

for a principal

Set a team convention separating local handling from global reporting, so the handler stays a signal of the truly unexpected.

## The rule behind the list Angular's guidance states the principle directly: errors should surface **at the call site** whenever possible. Angular therefore catches errors only where it is the caller and you had no place to wrap the call, and for asynchronous work only where it has an explicit contract to wait for the result **and** the API does not already expose the error as state. Everything else is your code's responsibility. ## Errors that reach `ErrorHandler` - **Constructors and lifecycle hooks** of components and directives, which Angular calls while creating and checking views. - **Template expressions** evaluated during change detection, for example a binding that reads a property of `undefined`. - **Template event listeners** such as `(click)="save()"`: Angular wraps the call and reports a thrown error. - **The `async` pipe**: if the Observable errors or the Promise rejects, the pipe reports it. Since v20 the pipe does this itself instead of relying on zone.js. - **`PendingTasks.run`**: a rejected function passed to it is reported. - **Bootstrap**: a failing initializer or root component creation is reported and rejects `bootstrapApplication`. - **Errors a `@boundary` catches** (developer preview): sent to the handler's optional `onViewError`, or to `handleError` if that method is absent. ## Errors that never get there on their own - **Errors you handled.** Once `try`/`catch` or `catchError` swallows an error, Angular never sees it. - **Errors returned as state.** `resource()` and its HTTP variant expose failure through `status` and `error` signals; reading them is how you handle it. - **Errors in calls you made directly.** If a service method throws inside your own `setTimeout`, promise chain or third-party callback, the stack no longer goes through Angular. - **RxJS subscriptions without an error callback.** RxJS rethrows such errors asynchronously so that they reach the runtime's global error mechanism, not Angular. - **A small startup window.** Before the root module or root component exists, Angular cannot look up your handler; the guide calls this out for custom elements upgraded during startup. ## Where zone.js and the global listeners come in The "never" list shrinks depending on setup: | Setup | Errors from your own async code (timers, promises, bare subscriptions) | |---|---| | Zone-based app | Errors in tasks running inside the Angular zone are forwarded to the handler through the zone | | Zoneless app (default since v21) without listeners | Only the browser console sees them | | Any app with `provideBrowserGlobalErrorListeners()` | The window `error` and `unhandledrejection` listeners forward them | The CLI adds `provideBrowserGlobalErrorListeners()` to new applications, so many projects already have it; older projects migrated to zoneless often do not. ## Why the distinction matters 1. **Monitoring coverage.** A custom handler that sends reports only sees the first list unless the global listeners are installed. 2. **Error design.** If an operation's failure should be visible to users, handle it where it happens; relying on the global handler means a generic message at best. 3. **Testing.** `TestBed` rethrows errors that reach the handler, so a throwing hook fails a test, while an error swallowed by `catchError` or kept in `resource().error()` does not. ## Worked examples | Scenario | Reaches `ErrorHandler`? | Why | |---|---|---| | `ngOnInit` calls a service that throws | Yes | The error propagates into a hook Angular called | | `{{ user.name }}` while `user` is `undefined` | Yes | Template expressions run during change detection | | `(click)="save()"` and `save()` throws | Yes | Template listeners are wrapped by Angular | | `http.get(...).subscribe(v => ...)` and the request fails | Only via zone.js or global listeners | No error callback, so RxJS rethrows asynchronously | | `resource()` loader rejects | No | The failure is stored in `error()` and `status()` | | `try { ... } catch { ... }` around a failing call | No | Your code already handled it | ## A quick classification exercise For any error, ask two questions: **who called the code that threw?** If Angular, the handler sees it. If you did, it propagates to your caller. And **did an API promise to deal with the result?** If it exposes the error as state, the handler does not see it; if it consumes the result for you, as the `async` pipe does, it reports the error.

  • A service method throws when called from ngOnInit. Does the handler see it?
    Yes. The error propagates out of the service into `ngOnInit`, and Angular called `ngOnInit`, so it catches the error and reports it. The same method called inside your own `setTimeout` would not go through Angular, and would reach the handler only via zone.js or the global listeners.
  • Why does the async pipe report errors but resource() does not?
    The `async` pipe consumes the stream for you and offers no place to handle a failure, so reporting is the only way to surface it. `resource()` exposes `status` and `error` as signals: the failure is part of its state, and the template is expected to render it.

saying these in an interview costs you the question

  • ErrorHandler receives every error thrown anywhere in the app
  • Errors from resource() are forwarded to ErrorHandler
  • A subscribe() without an error callback reports to ErrorHandler in every app
  • Errors in template event listeners bypass ErrorHandler
  • Zoneless apps catch timer errors without any extra provider