skip to content

When an Angular resolver's request fails or the invoice does not exist, what happens to the navigation, and how should the resolver redirect instead?

level: middleimportance: should knowfreq 48%

answer

  1. the navigation fails, user stays put
  2. NavigationError and the error handler
  3. RedirectCommand, not UrlTree
  4. catchError inside the resolver

basics

~20 s

An error thrown by a resolver fails the whole navigation: the router rolls back, emits NavigationError and leaves the user on the previous page. To send them elsewhere, the resolver returns (or, since 22.2, throws) a RedirectCommand.

solid answer

~30 s

If a resolver's Observable errors or its Promise rejects, the Angular router aborts the navigation, restores its previous state and emits `NavigationError`; the promise from `router.navigate()` rejects unless `resolveNavigationPromiseOnError` is set. You can centralise handling with `withNavigationErrorHandler()`, whose callback may return a `RedirectCommand` to turn the error into a redirect. Inside the resolver, catch the failure and return `new RedirectCommand(router.parseUrl('/invoices/not-found'))`; Angular 22.2 also accepts throwing it. A returned `UrlTree` does **not** redirect from a resolver: it just becomes the resolved data. Returning `EMPTY` is also wrong, because it cancels the navigation silently with `NoDataFromResolver`.

code

ts · 13 lines
ts
import { inject } from '@angular/core';
import { provideRouter, RedirectCommand, Router, withNavigationErrorHandler } from '@angular/router';
import { routes } from './app.routes';

export const routerProviders = [
  provideRouter(
    routes,
    withNavigationErrorHandler(() => {
      // any resolver or navigation error that was not handled locally
      return new RedirectCommand(inject(Router).parseUrl('/error'));
    }),
  ),
];

go deeper

for a junior

Know that a failing resolver stops the navigation, and that RedirectCommand is how a resolver sends the user to another page.

for a middle

Explain NavigationError, the rollback, withNavigationErrorHandler returning a RedirectCommand, and why a UrlTree or EMPTY does not work from a resolver.

for a senior

Decide per resolver between redirect and fallback, avoid redirect loops, and set one app-wide policy for unexpected navigation failures.

for a principal

Define how navigation failures surface to users and monitoring across the app, so no resolver can fail into a silent dead click.

## What a failing resolver does by default A resolver is part of the navigation pipeline, so its failure is the navigation's failure. When the returned `Observable` errors, the `Promise` rejects, or the function throws, the Angular router: 1. stops the navigation and **rolls back** its internal state, so the previous page stays active and the address bar is not updated to the target URL (with the default `urlUpdateStrategy` of `'deferred'`); 2. emits a **`NavigationError`** router event carrying the error and the target snapshot; 3. calls the handler registered with `withNavigationErrorHandler()`, if any; 4. rejects the promise returned by `router.navigate()` / `navigateByUrl()`, unless the router was configured with `resolveNavigationPromiseOnError: true`, in which case it resolves to `false`. For the user, the click on "Edit invoice" does nothing, with no message. That is why the router documentation insists on handling resolver errors explicitly. ## Three places to handle it | Where | How | Good for | |---|---|---| | Inside the resolver | `catchError` and return a `RedirectCommand` or a fallback value | a specific page such as "invoice not found" | | `withNavigationErrorHandler()` | a callback passed to `provideRouter`; runs in an injection context; may return a `RedirectCommand` | one app-wide policy, e.g. send any failed navigation to `/error` | | Router events | filter `Router.events` for `NavigationError` | banners, retry buttons, logging | When the error-handler callback returns a `RedirectCommand`, the router treats the failure as a redirect: it emits `NavigationCancel` with the redirect cancellation code instead of `NavigationError`. Any other return value is ignored. ## Redirecting from the resolver: `RedirectCommand` `RedirectCommand` is a class from `@angular/router` whose constructor takes a **`UrlTree`** and optional navigation behaviour options (such as `skipLocationChange` or `replaceUrl`). It was added so guards and resolvers could redirect with options. A resolver can use it two ways: - **return it** (as the value, or as the emission of its Observable) - the router checks each resolved value with `instanceof RedirectCommand` and redirects; - **throw it** - since **Angular 22.2** the router recognises a thrown `RedirectCommand` and redirects rather than reporting a `NavigationError`. ```ts export const invoiceResolver: ResolveFn<Invoice> = (route) => { const router = inject(Router); return inject(InvoiceApi).getInvoice(route.paramMap.get('id')!).pipe( catchError((err: HttpErrorResponse) => of(new RedirectCommand(router.parseUrl(err.status === 404 ? '/invoices/not-found' : '/error'))), ), ); }; ``` ## Two traps interviewers probe - **A `UrlTree` is not a redirect here.** Guards redirect when they return a `UrlTree`; resolvers do not. The resolver's value is stored as data, so the edit page opens with a `UrlTree` where the invoice should be. A resolver typed `ResolveFn<Invoice>` gets a compile error for this, but a loosely typed one compiles and fails at runtime. Only `RedirectCommand` redirects from a resolver. - **`EMPTY` is not a no-op.** An older pattern was `catchError(() => { router.navigate(['/error']); return EMPTY; })`. Completing without a value makes the router cancel the navigation with `NavigationCancellationCode.NoDataFromResolver`, and the manual `navigate()` starts a second navigation that the resolver never declared as a redirect. `RedirectCommand` does the same job as one coherent redirect. If several resolvers on the same route return a `RedirectCommand`, the router uses the first one returned; resolvers on one route have no ordering guarantee, so do not rely on which that is. ## Surfacing the failure to the user and to logs A redirect or fallback fixes the navigation, but the failure should still be visible somewhere. A common split is: the resolver decides what the user sees (a not-found page, a fallback value), while one listener on `Router.events` for `NavigationError`, or the `withNavigationErrorHandler()` callback, reports unexpected failures to the app's error tracking. Code that calls `router.navigate()` directly should either handle the rejected promise or the app should enable `resolveNavigationPromiseOnError`, otherwise every unhandled resolver error also appears as an unhandled promise rejection in the console. ## Choosing between redirect and fallback - Redirect when the page is meaningless without the data: a missing invoice should go to a not-found page. - Return a fallback value (for example an empty list of attachments) when the page can still work, and let the component show a notice. - Reserve the global error handler for unexpected failures, so resolver-specific decisions stay next to the data they concern. - Keep the redirect target free of the same resolver, or a failing API can bounce the user in a loop.

  • Why prefer RedirectCommand over calling router.navigate() inside catchError?
    Calling `navigate()` from the resolver starts a second navigation that supersedes the running one, and the resolver must still return something for the first, usually `EMPTY` or an error. A `RedirectCommand` instead tells the router to cancel the current navigation as a redirect and start the new one itself, with one coherent event sequence and options such as `replaceUrl` attached.
  • What does the address bar show after a resolver error with the default router settings?
    It keeps the previous URL. With the default `urlUpdateStrategy` of `'deferred'`, the router updates the browser URL only when the navigation succeeds, and on an error it rolls back its state, so the user stays on the page and URL they started from.

saying these in an interview costs you the question

  • Returning a UrlTree from a resolver redirects like a guard
  • An erroring resolver still activates the route with undefined data
  • Returning EMPTY after router.navigate() is a clean redirect
  • Resolver errors are caught by the component's error handling
  • withNavigationErrorHandler must rethrow the error to cancel navigation