In Angular, how do NavigationCancel, NavigationError and NavigationSkipped differ, and how do you tell why a navigation was cancelled?
answer
- deliberate stop versus failure
- cancellation code, not reason text
- GuardRejected, Redirect, Superseded
- skipped: same URL ignored
basics
~10 sNavigationCancel 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 linesimport { 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
Know that cancelled and failed navigations are different events, and that a guard returning false causes a cancel.
List the cancellation codes and what produces NavigationError, and explain why code should read event.code rather than reason.
Build navigation monitoring that separates expected cancellations from real errors and uses ids to follow overlapping navigations.
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