skip to content

Navigation Lifecycle Events

The Router.events sequence from NavigationStart through guards and resolvers to NavigationEnd, Cancel or Error, and the currentNavigation signal. Interviewers probe loading bars and failures.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In an Angular app, how do you drive a top-of-page loading bar from the router, and why does a NavigationStart/NavigationEnd pair leave it stuck?

level: juniorimportance: must knowfreq 58%

answer

  1. not only NavigationEnd ends it
  2. cancel and error end it too
  3. currentNavigation() is null when idle
  4. computed() in the root component

basics

~10 s

Show the bar while Router.currentNavigation() is non-null, or start it on NavigationStart and stop it on NavigationEnd, NavigationCancel or NavigationError. Stopping only on NavigationEnd leaves it running after cancelled or failed navigations.

solid answer

~40 s

The simplest current approach, available since v20.2, is a `computed(() => this.router.currentNavigation() !== null)` in the root component: the router sets that signal when a navigation begins and clears it when the navigation finishes in any way. With `Router.events`, you set the bar on `NavigationStart` and clear it on **any** terminal event, `NavigationEnd`, `NavigationCancel` or `NavigationError` (and `NavigationSkipped` is harmless to include). The classic bug is clearing only on `NavigationEnd`: a guard that returns `false`, a resolver that fails, or a navigation superseded by a newer click never emits `NavigationEnd`, so the bar spins forever. Redirects also show up as a cancel followed by a new start, so the bar can blink unless you smooth it.

code

ts · 18 lines
ts
import { Component, computed, inject } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `
    @if (navigating()) {
      <div class="top-loading-bar" role="progressbar" aria-label="Loading page"></div>
    }
    <router-outlet />
  `,
})
export class App {
  private readonly router = inject(Router);
  // null when idle; cleared after end, cancel, error or abort
  protected readonly navigating = computed(() => this.router.currentNavigation() !== null);
}

go deeper

for a junior

Know that the bar must stop on cancel and error as well as end, and that currentNavigation() gives a ready-made signal.

for a middle

Explain which situations end a navigation without NavigationEnd, and how redirects show up as cancel plus a new start.

for a senior

Build a bar that never sticks and never flickers: signal or full terminal-event handling, a short show delay, and redirect awareness.

for a principal

Treat navigation feedback as a shared shell concern with one owner, and set a latency budget beyond which pages must stop blocking navigation.

## What the loading bar needs to know A top-of-page progress bar answers one question: **is a navigation in flight right now?** Angular navigations can take visible time: lazy chunks download, guards call the server, resolvers fetch data. During all of that the old page stays on screen, so without a bar the click looks ignored. ## Option 1: the `currentNavigation` signal (v20.2+) `Router.currentNavigation` is a signal holding the current `Navigation` object, or `null` when the router is idle. The router sets it as each navigation begins and resets it to `null` when that navigation finishes, whether it succeeded, was cancelled, failed or was aborted. ```ts protected readonly navigating = computed(() => this.router.currentNavigation() !== null); ``` With signals, the template re-renders when the value changes, which also works with the `OnPush` default of v22 and zoneless change detection. This replaces the deprecated `getCurrentNavigation()` method, which was a plain getter and could not be used reactively. ## Option 2: `Router.events` In older code, or when you need per-phase detail, derive the flag from events: ```ts readonly navigating = toSignal( inject(Router).events.pipe( filter((e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError), map((e) => e instanceof NavigationStart), ), { initialValue: false }, ); ``` The essential part is the list of events that turn the bar **off**. ## Why start/end pairs get stuck Every navigation ends with exactly one of four events, and three of them are not `NavigationEnd`: | Situation | Terminal event | Bar if you only listen for `NavigationEnd` | |---|---|---| | guard returns `false` | `NavigationCancel` (`GuardRejected`) | stuck on | | user clicks another link before the first finishes | `NavigationCancel` (`SupersededByNewNavigation`) for the first, then the second ends normally | turns off only when the second ends | | resolver throws, chunk fails, no route matches | `NavigationError` | stuck on | | same URL, ignored | `NavigationSkipped`, with no `NavigationStart` | unaffected | The stuck bar is the classic bug. It is usually found after a guard starts redirecting unauthenticated users or an API starts failing. ## Redirects and flicker A guard redirect cancels the first navigation (`NavigationCancel`, code `Redirect`) and starts a new one. An events-based bar therefore turns off and on again between them. Two common refinements: - **Delay showing** the bar by 100-200 ms, so fast navigations never flash it; implement it with a small timer or an RxJS delay on the "on" value only. - **Ignore `Redirect` cancellations** when turning the bar off, since another navigation is about to start. ## Signal or events: which to use | Concern | `currentNavigation()` signal | `Router.events` | |---|---|---| | Available since | v20.2 | always | | Covers every ending | yes, reset to `null` on any finish | only if you list all terminal events | | Phase detail (guards, resolvers, lazy load) | read the `Navigation` object only | yes, per event | | Subscription teardown | none, it is a signal | needed in components (`takeUntilDestroyed`) | | Redirect blink | the old navigation is cleared and the new one set | cancel then start, visible unless smoothed | Phase detail is the main reason to keep an events-based variant. A bar can, for example, show a different label while a lazy chunk downloads (between `RouteConfigLoadStart` and `RouteConfigLoadEnd`) than while resolvers fetch data (between `ResolveStart` and `ResolveEnd`), which helps users and developers see where the time goes. For a plain on/off bar in current Angular, the signal is less code and cannot get stuck. ## Accessibility and placement - Put the bar in the root component, above the `<router-outlet>`, so it covers every route. - Give it `role="progressbar"` and an accessible label, and avoid moving focus to it. - Keep the logic in the root component or a small root service; per-page listeners duplicate it and leak if not torn down.

  • How would you avoid flashing the bar on navigations that finish in a few milliseconds?
    Delay only the transition to visible. With events, map `NavigationStart` to `true` and the terminal events to `false`, then use `switchMap` so `true` becomes `timer(150).pipe(map(() => true))` while `false` passes through immediately. A fast navigation emits `false` before the timer fires, so the bar never appears.
  • Why is currentNavigation() preferred over getCurrentNavigation() for this?
    `currentNavigation` is a signal, so a `computed()` or template that reads it updates automatically when navigation starts and stops, including under zoneless change detection. `getCurrentNavigation()` returns a snapshot value with no change notification, and it has been deprecated since v20.2 in favour of the signal.

The loading bar is like a 'train departing' light: switch it off only when the train arrives and it stays lit for every train cancelled or diverted; the reliable signal is the one that goes dark whenever the platform is empty, whatever happened to the train.

saying these in an interview costs you the question

  • Turning the bar off on NavigationEnd covers every navigation
  • A guard returning false emits NavigationEnd with no URL change
  • currentNavigation() stays set after a cancelled navigation
  • getCurrentNavigation() updates the template reactively
  • A redirect keeps the loading bar in one continuous navigation
open as a page

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

level: middleimportance: should knowfreq 40%

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.

open as a page

In Angular, in what order does Router.events emit during a successful navigation, and which events can end a navigation?

level: middleimportance: should knowfreq 52%

basics

~10 s

A successful Angular navigation emits NavigationStart, RoutesRecognized, GuardsCheckStart, GuardsCheckEnd, ResolveStart, ResolveEnd, then NavigationEnd. Every navigation ends with exactly one of NavigationEnd, NavigationCancel, NavigationError or NavigationSkipped.

open as a page

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

level: seniorimportance: should knowfreq 36%

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.

open as a page

In Angular, what does the Router.currentNavigation() signal expose during a navigation, and how can code abort a pending navigation?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Router.currentNavigation() returns the in-flight Navigation, or null when idle: its id, initialUrl, extractedUrl, finalUrl (set once routes are recognised), trigger, extras such as state, and previousNavigation. Calling its abort() cancels the navigation with the Aborted code, if activation has not started.

open as a page