skip to content

In Angular, how do NavigationCancel, NavigationError and NavigationSkipped differ, and how do you tell why a navigation was cancelled?

level: middleimportance: should knowfreq 40%

answer

  1. deliberate stop versus failure
  2. cancellation code, not reason text
  3. GuardRejected, Redirect, Superseded
  4. skipped: same URL ignored

basics

~10 s

NavigationCancel means the router deliberately stopped the navigation, NavigationError means something threw, and NavigationSkipped means it was ignored. The cancel event's code, a NavigationCancellationCode such as GuardRejected or Redirect, says why.

solid answer

~40 s

`NavigationCancel` is an expected outcome: a guard returned `false` (`GuardRejected`), a guard or resolver redirected (`Redirect`), a newer navigation replaced this one (`SupersededByNewNavigation`), a resolver completed without a value (`NoDataFromResolver`), or `Navigation.abort()` was called (`Aborted`). Read `event.code` for that; the `reason` string is a developer message that is often empty in production builds and may change. `NavigationError` is unexpected: no route matched, a guard or resolver threw, a lazy chunk failed to load; it carries `error` and the target snapshot, and by default the `navigate()` promise rejects. `NavigationSkipped` means the router ignored the navigation, with a `NavigationSkippedCode` such as `IgnoredSameUrlNavigation`, and it is not preceded by `NavigationStart`.

code

ts · 24 lines
ts
import { Injectable, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
  NavigationCancel,
  NavigationCancellationCode,
  NavigationError,
  NavigationSkipped,
  Router,
} from '@angular/router';

@Injectable({ providedIn: 'root' })
export class NavigationOutcomeLogger {
  constructor() {
    inject(Router).events.pipe(takeUntilDestroyed()).subscribe((e) => {
      if (e instanceof NavigationCancel && e.code === NavigationCancellationCode.GuardRejected) {
        console.warn('blocked by a guard', e.url);
      } else if (e instanceof NavigationError) {
        console.error('navigation failed', e.url, e.error);
      } else if (e instanceof NavigationSkipped) {
        console.debug('navigation ignored', e.url, e.code);
      }
    });
  }
}

go deeper

for a junior

Know that cancelled and failed navigations are different events, and that a guard returning false causes a cancel.

for a middle

List the cancellation codes and what produces NavigationError, and explain why code should read event.code rather than reason.

for a senior

Build navigation monitoring that separates expected cancellations from real errors and uses ids to follow overlapping navigations.

for a principal

Define which navigation outcomes count as incidents and which are product signals, so dashboards and alerts drive the right follow-up.

## Three different outcomes | Event | Nature | Key properties | `navigate()` promise | |---|---|---|---| | `NavigationCancel` | deliberate stop | `code` (`NavigationCancellationCode`), `reason` | resolves `false` (for a redirect it follows the new navigation) | | `NavigationError` | unexpected failure | `error`, `target` snapshot | rejects, unless `resolveNavigationPromiseOnError` is set | | `NavigationSkipped` | ignored, nothing happened | `code` (`NavigationSkippedCode`), `reason` | resolves `false` | The distinction matters in monitoring: cancellations are normal traffic (users double-click, guards redirect), while errors are defects or outages worth alerting on. ## Why a navigation was cancelled: `NavigationCancellationCode` The enum has five values in Angular 22.2: - **`GuardRejected`** - a guard returned `false`. `GuardsCheckEnd` was emitted first with `shouldActivate: false`. - **`Redirect`** - a guard or resolver returned (or, since 22.2, threw) a `RedirectCommand`, or a guard returned a `UrlTree`. A new navigation to the target follows. - **`SupersededByNewNavigation`** - a newer navigation started before this one finished, for example a second click. - **`NoDataFromResolver`** - a resolver's Observable completed without emitting. - **`Aborted`** - code called `abort()` on the `Navigation` object (available since v20). Use the **`code`**, not the `reason` text: the router documents `code` as the stable identifier and warns that `reason` can change; most of its descriptive messages are only built in development mode, so `reason` is often empty in production. ## What produces `NavigationError` - No route matches the URL and there is no `**` wildcard; the error is the router's "Cannot match any routes" runtime error (code NG04002). - A guard, resolver, `redirectTo` function or loader **throws** or its Observable errors. - A `loadComponent` or `loadChildren` chunk fails to download. The event carries the `error` and, when available, the `target` router state. The router rolls back to the previous state, and `withNavigationErrorHandler()` gets a chance to convert the error into a redirect by returning a `RedirectCommand`; if it does, the router emits `NavigationCancel` with code `Redirect` instead of `NavigationError`. ## When `NavigationSkipped` appears It was added in v15.1 for navigations the router decides not to process: 1. **`IgnoredSameUrlNavigation`** - the target equals the current URL and `onSameUrlNavigation` is `'ignore'` (the default). 2. **`IgnoredByUrlHandlingStrategy`** - a custom `UrlHandlingStrategy` says neither the current nor the target URL belongs to Angular. A same-URL skip is emitted **without** a `NavigationStart`, so it does not disturb start/stop pairing, but analytics code that counts clicks through `NavigationStart` will not see it. ## Reacting to each outcome in the UI The three outcomes deserve different treatment on screen: - **`NavigationCancel` with `GuardRejected`**: the user tried to go somewhere they may not; a short message such as a toast explaining why is often better than silence. With `Redirect`, do nothing, because the redirect target is already loading. - **`NavigationCancel` with `SupersededByNewNavigation`**: never show anything; the user simply clicked again. - **`NavigationError`**: show an error banner with a retry action, or let a central `withNavigationErrorHandler()` redirect to an error page. Retrying means navigating to the failed `url` again. - **`NavigationSkipped`**: nothing happened, so nothing to show; if a same-URL click should refresh data, that is a configuration choice (`onSameUrlNavigation: 'reload'`), not an error. Clear any error banner on the next `NavigationStart`, so a message from a failed navigation does not linger over the next, successful one. ## A diagnostic recipe 1. Filter `Router.events` for the three outcome types and log `id`, `url` and `code` or `error`. 2. In development, `withDebugTracing()` prints the full sequence, so you can see which phase came last before the cancel or error. 3. Treat `GuardRejected` spikes as a UX or auth problem, `NoDataFromResolver` as a resolver bug, `SupersededByNewNavigation` as usually harmless, and any `NavigationError` as a defect to fix.

  • Why should monitoring alert on NavigationError but not on every NavigationCancel?
    Most cancellations are expected: a user clicks twice (`SupersededByNewNavigation`), a guard redirects to login (`Redirect`), or denies access (`GuardRejected`). `NavigationError` means code threw, a chunk failed to load, or no route matched, which points to a bug or an outage. Alerting on all cancellations buries the real failures in normal traffic.
  • What does NavigationCancel carry when a newer click supersedes a navigation?
    The first navigation ends with `NavigationCancel` whose `code` is `NavigationCancellationCode.SupersededByNewNavigation`; the newer navigation continues with its own id and ends with its own terminal event. Listeners that track progress should key on the event `id` so the cancel of the old navigation does not hide the new one.

saying these in an interview costs you the question

  • A guard returning false emits NavigationError
  • The reason string is a stable production identifier
  • NavigationSkipped always follows a NavigationStart
  • A redirect from a guard is reported as NavigationError
  • An unmatched URL is silently cancelled with no event