skip to content

In Angular 22, how do you handle navigation errors centrally with withNavigationErrorHandler, and what can that handler not change?

level: seniorimportance: should knowfreq 36%

answer

  1. a provideRouter feature
  2. receives the NavigationError
  3. runs in an injection context
  4. only RedirectCommand changes the outcome
  5. promise rejection is a separate option

basics

~10 s

Pass withNavigationErrorHandler(fn) to provideRouter; fn receives each NavigationError and runs in an injection context. Returning a RedirectCommand turns the error into a redirect; anything else is ignored, and it cannot stop navigate() from rejecting.

solid answer

~40 s

`provideRouter(routes, withNavigationErrorHandler((error) => ...))` registers one callback that the router calls with the `NavigationError` whenever a navigation fails: no route matched, a guard or resolver threw, a lazy chunk failed. It runs inside the router's environment injection context, so it can `inject()` a logger or the `Router`. If it returns a `RedirectCommand`, the router converts the failure into a redirect: it emits `NavigationCancel` with code `Redirect` instead of `NavigationError` and navigates to the command's target. Any other return value is ignored and the `NavigationError` event is emitted as usual. It cannot change the promise returned by `navigate()`; to stop that promise rejecting you set `resolveNavigationPromiseOnError`. The older `Router.errorHandler` property was removed in v19.

code

ts · 26 lines
ts
import { ApplicationConfig, inject } from '@angular/core';
import {
  NavigationError,
  RedirectCommand,
  Router,
  provideRouter,
  withNavigationErrorHandler,
  withRouterConfig,
} from '@angular/router';
import { routes } from './app.routes';
import { NavigationTelemetry } from './navigation-telemetry';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withNavigationErrorHandler((error: NavigationError) => {
        inject(NavigationTelemetry).record(error.url, error.error);
        // converts the failure into a redirect: NavigationCancel(Redirect), not NavigationError
        return new RedirectCommand(inject(Router).parseUrl('/something-went-wrong'), { skipLocationChange: true });
      }),
      // separate concern: make navigate() resolve false instead of rejecting
      withRouterConfig({ resolveNavigationPromiseOnError: true }),
    ),
  ],
};

go deeper

for a junior

Know that provideRouter accepts a navigation error handler, and that it runs when a navigation fails rather than when it is cancelled.

for a middle

Explain that only a returned RedirectCommand changes the outcome, that inject() works inside it, and what resolveNavigationPromiseOnError is for.

for a senior

Design error handling in layers: local redirects in resolvers, a wildcard route, one global handler with telemetry, and no competing listeners.

for a principal

Set the policy for how navigation failures surface to users and to monitoring, and who owns the global handler as the app grows.

## What the feature is `withNavigationErrorHandler()` is a **router feature function** passed to `provideRouter` (the NgModule equivalent is the `errorHandler` option of `RouterModule.forRoot`). It gives the application one place to react to navigation failures. ```ts provideRouter( routes, withNavigationErrorHandler((error: NavigationError) => { inject(NavigationTelemetry).record(error.url, error.error); return new RedirectCommand(inject(Router).parseUrl('/something-went-wrong')); }), ); ``` ## When it runs The handler runs when a navigation **errors**, not when it is cancelled or skipped. Typical triggers: - no route matches the URL and there is no `**` wildcard (the router's NG04002 no-match error); - a guard, resolver or `redirectTo` function throws, or its Observable errors; - a lazy `loadComponent` or `loadChildren` chunk fails to load. A guard that returns `false` or a `UrlTree` is a **cancellation**, so the handler is not called for it. The router calls the handler with `runInInjectionContext` on its environment injector, so `inject()` works inside it. It is called **after** the router has rolled back its state, with the `NavigationError` object (`id`, `url`, `error`, `target`). ## The one return value that matters | Handler returns | What the router emits | Outcome | |---|---|---| | a `RedirectCommand` | `NavigationCancel` with code `Redirect`, then a new navigation | the user lands on the redirect target | | anything else, or nothing | `NavigationError` | the user stays on the previous page | So returning a `UrlTree`, `true` or `false` changes nothing: only `RedirectCommand` converts the error. This conversion has existed since v18. A `RedirectCommand` also carries navigation options such as `skipLocationChange` or `replaceUrl`, which is useful for an error page that should not add a history entry. ## What it cannot do 1. **It cannot resolve the `navigate()` promise.** When a navigation errors, `router.navigate()` and `navigateByUrl()` reject by default. Since v19 the router documents that the error handler cannot change that; set `resolveNavigationPromiseOnError: true` through `withRouterConfig()` (or `RouterModule.forRoot` options) if callers should get `false` instead of a rejection. 2. **It does not see cancellations.** For guard rejections or superseded navigations, listen to `NavigationCancel` on `Router.events`. 3. **It is not the application `ErrorHandler`.** Errors thrown later, during rendering or in event handlers, go to Angular's global `ErrorHandler`, not here. 4. **It is not a retry mechanism.** It can redirect, including to the same URL, but a blind redirect to the failing URL loops. ## Handler or events listener? Both see navigation errors, but they fit different jobs: | | `withNavigationErrorHandler()` | `Router.events` listener for `NavigationError` | |---|---|---| | Registered | once, in `provideRouter` | wherever you subscribe | | Can change the outcome | yes, by returning a `RedirectCommand` | no, it only observes | | Sees the error when | before `NavigationError` is emitted | when the event is emitted | | Injection context | yes | the subscriber's own context | | Typical use | global redirect policy and telemetry | UI banners, retry buttons | A sound combination is one handler that decides policy (redirect or not) and records the failure, plus a UI component listening for `NavigationError` to show a banner when the handler chose not to redirect. If the handler redirects, no `NavigationError` is emitted, so the banner and the redirect never compete. ## Migrating older code - `Router.errorHandler` (assigning a function to the router instance) was **removed in v19**; use `withNavigationErrorHandler()` or the `errorHandler` option. - Code that caught the rejected `navigate()` promise everywhere can often move to one handler plus `resolveNavigationPromiseOnError`. - Per-page `NavigationError` subscriptions that each redirect to an error page should be consolidated, or two listeners race to navigate. ## A reasonable production setup - The handler records the error with the URL and navigation id, then returns a `RedirectCommand` to a generic error route for unexpected failures. - Specific, expected failures, such as a missing record, are handled closer to the source: a resolver returns a `RedirectCommand` to a not-found page, so they never reach the global handler. - A `**` wildcard route turns unknown URLs into a not-found page before they become errors at all.

  • Why does the handler use skipLocationChange in its RedirectCommand?
    With `skipLocationChange: true` the router shows the error route without writing its URL to the address bar or history. The user still sees the URL they attempted, can copy or retry it, and pressing Back does not step through an extra error-page entry.
  • A guard returns false and nothing reaches the error handler. Is that a bug?
    No. A guard returning `false` is a cancellation, reported as `NavigationCancel` with code `GuardRejected`, and `withNavigationErrorHandler()` only receives `NavigationError`. To react to rejections, listen for `NavigationCancel` on `Router.events`, or have the guard return a `RedirectCommand` so the user is sent somewhere useful.

saying these in an interview costs you the question

  • Returning a UrlTree from the handler redirects the user
  • The handler also runs for guard rejections
  • The handler can make navigate() resolve instead of reject
  • inject() cannot be used inside the error handler
  • Router.errorHandler is still the recommended hook