# Router events

You can use the lifecycle hooks ([instance](https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle) and [shared](https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-hooks)) to intercept different stages of the navigation when you are working with the routed components directly. However, if you want to tap into different navigation phases from a non-routed component, such as standalone service or a simple custom element, then you need to leverage router events. This section discusses that.

## Router Event Types Overview

The router emits five distinct events that cover the complete navigation lifecycle:

| Event                         | When Emitted                             | Use Cases                                          |
| ----------------------------- | ---------------------------------------- | -------------------------------------------------- |
| `au:router:location-change`   | Browser location changed via history API | Track URL changes, analytics, browser navigation   |
| `au:router:navigation-start`  | Before navigation begins                 | Show loading states, cancel navigation, logging    |
| `au:router:navigation-end`    | Navigation completes successfully        | Hide loading states, update breadcrumbs, analytics |
| `au:router:navigation-cancel` | Navigation cancelled by guards/hooks     | Handle cancelled navigation, show messages         |
| `au:router:navigation-error`  | Navigation encounters an error           | Error handling, fallback routing, logging          |

## Event Details and Properties

### `LocationChangeEvent`

Triggered when the browser location changes through user navigation (back/forward buttons) or hash changes.

```typescript
interface LocationChangeEvent {
  readonly id: number;           // Unique navigation ID
  readonly url: string;          // New URL
  readonly trigger: 'popstate' | 'hashchange';  // What caused the change
  readonly state: {} | null;     // Browser history state
}
```

### `NavigationStartEvent`

Emitted before navigation execution begins, giving you a chance to prepare or cancel.

```typescript
interface NavigationStartEvent {
  readonly id: number;                    // Unique navigation ID
  readonly instructions: ViewportInstructionTree;  // Where we're navigating
  readonly trigger: 'popstate' | 'hashchange' | 'api';  // What triggered navigation
  readonly managedState: ManagedState | null;     // Router-managed state
}
```

### `NavigationEndEvent`

Fired when navigation completes successfully, providing final instruction details.

```typescript
interface NavigationEndEvent {
  readonly id: number;                    // Unique navigation ID
  readonly instructions: ViewportInstructionTree;      // Original instructions
  readonly finalInstructions: ViewportInstructionTree; // Final resolved instructions
}
```

### `NavigationCancelEvent`

Emitted when navigation is cancelled by lifecycle hooks (for example by returning `false` or returning a redirect instruction).

```typescript
interface NavigationCancelEvent {
  readonly id: number;                    // Unique navigation ID
  readonly instructions: ViewportInstructionTree;  // Attempted instructions
  readonly reason: unknown;               // Cancellation reason
}
```

### `NavigationErrorEvent`

Triggered when navigation encounters errors during execution.

```typescript
interface NavigationErrorEvent {
  readonly id: number;                    // Unique navigation ID
  readonly instructions: ViewportInstructionTree;  // Failed instructions
  readonly error: unknown;                // The error that occurred
}
```

## Subscribing to Router Events

You can subscribe to router events in two ways: using the event aggregator or the type-safe `IRouterEvents` service (recommended).

### Type-Safe Event Subscription with `IRouterEvents`

The recommended approach uses `IRouterEvents` for compile-time type safety and better developer experience:

```typescript
import {
  IRouterEvents,
  LocationChangeEvent,
  NavigationStartEvent,
  NavigationEndEvent,
  NavigationCancelEvent,
  NavigationErrorEvent,
} from '@aurelia/router';
import { IDisposable, resolve } from '@aurelia/kernel';

export class NavigationService implements IDisposable {
  private readonly subscriptions: IDisposable[] = [];
  private currentNavigationId: number | null = null;

  public constructor() {
    const events = resolve(IRouterEvents);
    
    this.subscriptions = [
      // Track location changes from browser navigation
      events.subscribe('au:router:location-change', (event: LocationChangeEvent) => {
        console.log(`Location changed: ${event.url} via ${event.trigger}`);
        this.handleLocationChange(event);
      }),

      // Prepare for navigation start
      events.subscribe('au:router:navigation-start', (event: NavigationStartEvent) => {
        this.currentNavigationId = event.id;
        console.log(`Navigation #${event.id} starting to: ${event.instructions}`);
        this.handleNavigationStart(event);
      }),

      // Handle successful navigation completion
      events.subscribe('au:router:navigation-end', (event: NavigationEndEvent) => {
        console.log(`Navigation #${event.id} completed successfully`);
        this.handleNavigationEnd(event);
        this.currentNavigationId = null;
      }),

      // Handle cancelled navigation
      events.subscribe('au:router:navigation-cancel', (event: NavigationCancelEvent) => {
        console.warn(`Navigation #${event.id} cancelled:`, event.reason);
        this.handleNavigationCancel(event);
        this.currentNavigationId = null;
      }),

      // Handle navigation errors
      events.subscribe('au:router:navigation-error', (event: NavigationErrorEvent) => {
        console.error(`Navigation #${event.id} failed:`, event.error);
        this.handleNavigationError(event);
        this.currentNavigationId = null;
      }),
    ];
  }

  private handleLocationChange(event: LocationChangeEvent): void {
    // Update analytics, breadcrumbs, etc.
  }

  private handleNavigationStart(event: NavigationStartEvent): void {
    // Show loading indicators, prepare UI state
  }

  private handleNavigationEnd(event: NavigationEndEvent): void {
    // Hide loading indicators, update UI state
  }

  private handleNavigationCancel(event: NavigationCancelEvent): void {
    // Show user feedback, restore previous state
  }

  private handleNavigationError(event: NavigationErrorEvent): void {
    // Show error messages, log errors, fallback routing
  }

  public dispose(): void {
    this.subscriptions.forEach(subscription => subscription.dispose());
    this.subscriptions.length = 0;
  }
}
```

### Alternative: Event Aggregator Subscription

You can also use the standard event aggregator, though you lose TypeScript type safety:

```typescript
import { IEventAggregator, resolve } from '@aurelia/kernel';

export class BasicNavigationService {
  private readonly ea: IEventAggregator = resolve(IEventAggregator);

  public constructor() {
    this.ea.subscribe('au:router:navigation-start', (event: any) => {
      // Note: 'event' is typed as 'any' - no type safety
    });
  }
}
```

**Important:** Using `IRouterEvents` provides type safety and IntelliSense support, making it the preferred approach.

## Practical Use Cases and Examples

### Leverage managed history state

The router stores a tiny object inside every browser history entry it creates. That object always contains an `au-nav-id` field (exported as `AuNavId`) so the router can determine whether a later `popstate` represents a backward or forward navigation. You can read and extend that managed state through the router events API.

#### Read managed state when navigation starts

```typescript
import { IRouterEvents, NavigationStartEvent } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class NavigationStateLogger {
  private readonly events = resolve(IRouterEvents);

  public constructor() {
    this.events.subscribe('au:router:navigation-start', (event: NavigationStartEvent) => {
      if (event.managedState) {
        console.log('Entry id:', event.managedState['au-nav-id']);
        console.log('Custom payload:', event.managedState['filters']);
      }
    });
  }
}
```

* The `managedState` payload is populated for both API-driven and browser-driven navigations, but only browser-triggered events will contain data you previously stored in `history.state`.
* The `au-nav-id` key is required—always merge existing state instead of overwriting it.
* See [Router state management](https://docs.aurelia.io/getting-to-know-aurelia/advanced/router-state-management#managed-history-entries-au-nav-id-and-managedstate) for end-to-end scenarios, including persisting filters or scroll depth.

#### Store additional metadata per history entry

```typescript
import { IRouterEvents, NavigationEndEvent } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class HistoryMetadataWriter {
  private readonly events = resolve(IRouterEvents);

  public constructor() {
    this.events.subscribe('au:router:navigation-end', (event: NavigationEndEvent) => {
      const nextState = {
        ...(window.history.state ?? {}),
        filters: this.getActiveFilters(),
        updatedAt: Date.now(),
      };
      window.history.replaceState(nextState, document.title);
    });
  }

  private getActiveFilters() {
    return { tab: 'inbox' };
  }
}
```

The router will emit the same metadata the next time that history entry is restored, allowing you to resume UI state in `NavigationStartEvent` or `NavigationEndEvent` handlers.

### Global Loading Indicator

Show a loading spinner during navigation:

```typescript
import { resolve } from '@aurelia/kernel';
import { customElement, observable } from '@aurelia/runtime-html';
import { IRouter, IRouterEvents, NavigationStartEvent, NavigationEndEvent } from '@aurelia/router';

@customElement({
  name: 'loading-app',
  template: `
    <div class="app-container">
      <!-- Global loading indicator -->
      <div if.bind="isNavigating" class="loading-overlay">
        <div class="spinner"></div>
        <span>Loading...</span>
      </div>
      
      <!-- Navigation breadcrumbs -->
      <nav class="breadcrumb" if.bind="breadcrumbs.length">
        <span repeat.for="crumb of breadcrumbs" class="breadcrumb-item">
          \${crumb}
        </span>
      </nav>
      
      <!-- Main content -->
      <au-viewport></au-viewport>
    </div>
  `
})
export class LoadingApp {
  @observable isNavigating: boolean = false;
  @observable breadcrumbs: string[] = [];
  private readonly router = resolve(IRouter);

  private readonly subscriptions = [
    resolve(IRouterEvents).subscribe('au:router:navigation-start', (event: NavigationStartEvent) => {
      this.isNavigating = true;
      console.log(`Starting navigation to: ${event.instructions.toUrl(false, this.router.options._urlParser, true)}`);
    }),

    resolve(IRouterEvents).subscribe('au:router:navigation-end', (event: NavigationEndEvent) => {
      this.isNavigating = false;
      this.updateBreadcrumbs();
      console.log(`Navigation completed: ${event.finalInstructions.toUrl(true, this.router.options._urlParser, true)}`);
    }),

    resolve(IRouterEvents).subscribe('au:router:navigation-cancel', () => {
      this.isNavigating = false;
      console.log('Navigation was cancelled');
    }),

    resolve(IRouterEvents).subscribe('au:router:navigation-error', (event) => {
      this.isNavigating = false;
      this.handleNavigationError(event.error);
    })
  ];

  private updateBreadcrumbs(): void {
    const routeTree = this.router.routeTree;
    const separator = ' › ';
    this.breadcrumbs = routeTree.root.children
      .map(node => {
        const title = node.getTitle(separator);
        const path = node.computeAbsolutePath();
        return title ?? (path.length > 0 ? path : 'Unknown');
      });
  }

  private handleNavigationError(error: unknown): void {
    console.error('Navigation failed:', error);
    // Could show toast notification, redirect to error page, etc.
  }

  dispose(): void {
    this.subscriptions.forEach(sub => sub.dispose());
  }
}
```

### Analytics and Tracking Service

Track navigation events for analytics:

```typescript
import { singleton, resolve } from '@aurelia/kernel';
import { IRouter, IRouterEvents, NavigationEndEvent, LocationChangeEvent } from '@aurelia/router';

@singleton
export class AnalyticsService {
  private readonly startTimes = new Map<number, number>();
  private readonly router = resolve(IRouter);

  public constructor() {
    const events = resolve(IRouterEvents);

    // Track page views
    events.subscribe('au:router:navigation-end', (event: NavigationEndEvent) => {
      this.trackPageView(event);
      this.trackNavigationTiming(event);
    });

    // Track browser navigation
    events.subscribe('au:router:location-change', (event: LocationChangeEvent) => {
      this.trackLocationChange(event);
    });

    // Track navigation starts for timing
    events.subscribe('au:router:navigation-start', (event) => {
      this.startTimes.set(event.id, performance.now());
    });
  }

  private trackPageView(event: NavigationEndEvent): void {
    const url = event.finalInstructions.toUrl(true, this.router.options._urlParser, true);
    const title = this.extractPageTitle();
    
    // Send to analytics service (Google Analytics, Adobe Analytics, etc.)
    if (typeof gtag !== 'undefined') {
      gtag('config', 'GA_TRACKING_ID', {
        page_title: title,
        page_location: window.location.href
      });
    }

    console.log(`📊 Page view: ${title} (${url})`);
  }

  private trackNavigationTiming(event: NavigationEndEvent): void {
    const startTime = this.startTimes.get(event.id);
    if (startTime) {
      const duration = performance.now() - startTime;
      console.log(`⏱️ Navigation #${event.id} took ${duration.toFixed(2)}ms`);
      
      // Track slow navigations 
      if (duration > 1000) {
        console.warn(`🐌 Slow navigation detected: ${duration.toFixed(2)}ms`);
      }

      this.startTimes.delete(event.id);
    }
  }

  private trackLocationChange(event: LocationChangeEvent): void {
    console.log(`🔄 Location changed via ${event.trigger}: ${event.url}`);
    
    // Track back/forward button usage
    if (event.trigger === 'popstate') {
      // Send analytics event for browser navigation
    }
  }

  private extractPageTitle(): string {
    return this.router.routeTree.root.getTitle(' | ') ?? 'Unknown Page';
  }
}
```

### Error Handling and Recovery Service

Handle navigation errors gracefully:

```typescript
import { singleton, resolve } from '@aurelia/kernel';
import { IRouter, IRouterEvents, NavigationErrorEvent, NavigationCancelEvent } from '@aurelia/router';

interface ErrorRecoveryStrategy {
  shouldRecover(error: unknown): boolean;
  recover(error: unknown): Promise<void>;
}

@singleton
export class NavigationErrorService {
  private readonly router: IRouter = resolve(IRouter);
  private errorHistory: Array<{timestamp: number, error: unknown, url: string}> = [];

  private recoveryStrategies: ErrorRecoveryStrategy[] = [
    {
      shouldRecover: (error) => error instanceof Error && error.message.includes('Component not found'),
      recover: async (error) => {
        console.log('Component not found, redirecting to home');
        await this.router.load('/');
      }
    },
    {
      shouldRecover: (error) => error instanceof Error && error.message.includes('Permission denied'),
      recover: async (error) => {
        console.log('Permission denied, redirecting to login');
        await this.router.load('/login');
      }
    }
  ];

  public constructor() {
    const events = resolve(IRouterEvents);

    events.subscribe('au:router:navigation-error', (event: NavigationErrorEvent) => {
      this.handleNavigationError(event);
    });

    events.subscribe('au:router:navigation-cancel', (event: NavigationCancelEvent) => {
      this.handleNavigationCancel(event);
    });
  }

  private async handleNavigationError(event: NavigationErrorEvent): Promise<void> {
    const url = event.instructions.toUrl(false, this.router.options._urlParser, true);
    
    // Log error for debugging
    this.errorHistory.push({
      timestamp: Date.now(),
      error: event.error,
      url
    });

    console.error(`❌ Navigation error for ${url}:`, event.error);

    // Try recovery strategies
    for (const strategy of this.recoveryStrategies) {
      if (strategy.shouldRecover(event.error)) {
        try {
          await strategy.recover(event.error);
          console.log(`✅ Recovered from navigation error using strategy`);
          return;
        } catch (recoveryError) {
          console.error('Recovery strategy failed:', recoveryError);
        }
      }
    }

    // Fallback: show error page or go to home
    this.showErrorNotification(`Navigation failed: ${url}`);
    // If you need to pass error details to the error page, store them in a service/store.
    await this.router.load('/error', {
      queryParams: { from: url },
    });
  }

  private handleNavigationCancel(event: NavigationCancelEvent): void {
    const url = event.instructions.toUrl(false, this.router.options._urlParser, true);
    console.warn(`⚠️ Navigation cancelled for ${url}:`, event.reason);
    
    // Show user-friendly message
    if (typeof event.reason === 'string' && event.reason.includes('permission')) {
      this.showErrorNotification('You do not have permission to access this page');
    } else {
      this.showErrorNotification('Navigation was cancelled');
    }
  }

  private showErrorNotification(message: string): void {
    // Implementation depends on your notification system
    console.log(`🔔 ${message}`);
  }

  public getErrorHistory(): Array<{timestamp: number, error: unknown, url: string}> {
    return [...this.errorHistory];
  }

  public clearErrorHistory(): void {
    this.errorHistory.length = 0;
  }
}
```

### Navigation State Management

Track and manage complex navigation states:

```typescript
import { singleton, resolve, observable } from '@aurelia/kernel';
import { IRouter, IRouterEvents, NavigationStartEvent, NavigationEndEvent } from '@aurelia/router';

interface NavigationHistoryEntry {
  id: number;
  url: string;
  timestamp: number;
  duration?: number;
  trigger: 'api' | 'popstate' | 'hashchange';
}

@singleton
export class NavigationStateService {
  @observable currentNavigation: NavigationHistoryEntry | null = null;
  @observable navigationHistory: NavigationHistoryEntry[] = [];
  @observable isNavigating: boolean = false;

  private readonly router = resolve(IRouter);
  private pendingNavigations = new Map<number, NavigationHistoryEntry>();

  public constructor() {
    const events = resolve(IRouterEvents);

    events.subscribe('au:router:navigation-start', (event: NavigationStartEvent) => {
      const entry: NavigationHistoryEntry = {
        id: event.id,
        url: event.instructions.toUrl(false, this.router.options._urlParser, true),
        timestamp: Date.now(),
        trigger: event.trigger
      };

      this.pendingNavigations.set(event.id, entry);
      this.currentNavigation = entry;
      this.isNavigating = true;
    });

    events.subscribe('au:router:navigation-end', (event: NavigationEndEvent) => {
      const entry = this.pendingNavigations.get(event.id);
      if (entry) {
        entry.duration = Date.now() - entry.timestamp;
        entry.url = event.finalInstructions.toUrl(true, this.router.options._urlParser, true); // Use final URL
        
        this.navigationHistory.push(entry);
        this.pendingNavigations.delete(event.id);
        
        // Keep only last 50 entries
        if (this.navigationHistory.length > 50) {
          this.navigationHistory.shift();
        }
      }

      this.isNavigating = false;
      this.currentNavigation = null;
    });

    events.subscribe('au:router:navigation-cancel', (event) => {
      this.pendingNavigations.delete(event.id);
      this.isNavigating = false;
      this.currentNavigation = null;
    });

    events.subscribe('au:router:navigation-error', (event) => {
      this.pendingNavigations.delete(event.id);
      this.isNavigating = false;
      this.currentNavigation = null;
    });
  }

  public getRecentNavigation(count: number = 10): NavigationHistoryEntry[] {
    return this.navigationHistory.slice(-count);
  }

  public getAverageNavigationTime(): number {
    const withDuration = this.navigationHistory.filter(entry => entry.duration);
    if (withDuration.length === 0) return 0;
    
    const total = withDuration.reduce((sum, entry) => sum + (entry.duration || 0), 0);
    return total / withDuration.length;
  }

  public getNavigationStats() {
    return {
      totalNavigations: this.navigationHistory.length,
      averageTime: this.getAverageNavigationTime(),
      currentlyNavigating: this.isNavigating,
      triggerStats: this.getTriggerStats()
    };
  }

  private getTriggerStats() {
    return this.navigationHistory.reduce((stats, entry) => {
      stats[entry.trigger] = (stats[entry.trigger] || 0) + 1;
      return stats;
    }, {} as Record<string, number>);
  }
}
```

## Best Practices for Router Events

### Memory Management

Always dispose of event subscriptions to prevent memory leaks:

```typescript
import { IDisposable, resolve } from '@aurelia/kernel';
import { IRouterEvents } from '@aurelia/router';

export class Component implements IDisposable {
  private readonly subscriptions: IDisposable[] = [];

  constructor() {
    const events = resolve(IRouterEvents);
    
    this.subscriptions.push(
      events.subscribe('au:router:navigation-start', (event) => {
        // Handle event
      })
    );
  }

  dispose(): void {
    this.subscriptions.forEach(sub => sub.dispose());
    this.subscriptions.length = 0;
  }
}
```

### Performance Considerations

1. **Debounce expensive operations** in event handlers
2. **Use singleton services** for global event handlers
3. **Unsubscribe** when components are disposed
4. **Avoid heavy computations** in event handlers

### Error Handling in Event Handlers

Always handle errors in event subscribers:

```typescript
events.subscribe('au:router:navigation-end', (event) => {
  try {
    // Your event handling logic
    this.updateUI(event);
  } catch (error) {
    console.error('Error in navigation-end handler:', error);
    // Don't let handler errors break navigation
  }
});
```

### Debugging Router Events

Enable detailed logging for debugging:

```typescript
import { resolve } from '@aurelia/kernel';
import { IRouter, IRouterEvents } from '@aurelia/router';

export class RouterDebugService {
  constructor() {
    const router = resolve(IRouter);
    const events = resolve(IRouterEvents);
    
    if (process.env.NODE_ENV === 'development') {
      events.subscribe('au:router:location-change', (event) => {
        console.group(`🔄 Location Change #${event.id}`);
        console.log('URL:', event.url);
        console.log('Trigger:', event.trigger);
        console.log('State:', event.state);
        console.groupEnd();
      });

      events.subscribe('au:router:navigation-start', (event) => {
        console.group(`🚀 Navigation Start #${event.id}`);
        console.log('Instructions:', event.instructions.toString());
        console.log('Trigger:', event.trigger);
        console.groupEnd();
      });

      events.subscribe('au:router:navigation-end', (event) => {
        console.group(`✅ Navigation End #${event.id}`);
        console.log('Final URL:', event.finalInstructions.toUrl(true, router.options._urlParser, true));
        console.groupEnd();
      });

      events.subscribe('au:router:navigation-cancel', (event) => {
        console.group(`❌ Navigation Cancel #${event.id}`);
        console.log('Reason:', event.reason);
        console.groupEnd();
      });

      events.subscribe('au:router:navigation-error', (event) => {
        console.group(`💥 Navigation Error #${event.id}`);
        console.error('Error:', event.error);
        console.groupEnd();
      });
    }
  }
}
```

## Using Current Route for Simple Cases

For simple scenarios where you only need current route information without complex event handling, use `ICurrentRoute`:

```typescript
import { resolve } from '@aurelia/kernel';
import { ICurrentRoute } from '@aurelia/router';

export class SimpleComponent {
  private readonly currentRoute: ICurrentRoute = resolve(ICurrentRoute);

  get currentPath(): string {
    return this.currentRoute.path;
  }

  get currentUrl(): string {
    return this.currentRoute.url;
  }

  get routeTitle(): string {
    return this.currentRoute.title;
  }
}
```

See [Current route](https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/navigation/current-route) for detailed information about the `ICurrentRoute` service.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aurelia.io/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-events.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
