Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions adev/src/app/routing/navigation-entries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,12 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
contentPath: 'guide/routing/data-fetching-with-resources',
status: 'new',
},
{
label: 'Error boundaries',
path: 'guide/routing/error-boundaries',
contentPath: 'guide/routing/error-boundaries',
status: 'new',
},
{
label: 'Lifecycle and events',
path: 'guide/routing/lifecycle-and-events',
Expand Down
20 changes: 20 additions & 0 deletions adev/src/content/guide/routing/define-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,26 @@ When you need to fetch data for a route, Angular Router supports reactive route
- [Data fetching with resources](/guide/routing/data-fetching-with-resources): Fetch data reactively using Angular Signals `Resource` APIs.
- [Route data resolvers](/guide/routing/data-resolvers): Fetch data before route activation using resolver functions.

### Error fallback components

You can specify an `errorComponent` on a route to render a dedicated fallback component if the route component throws an error during initialization or rendering:

```ts
import {Routes} from '@angular/router';
import {ProductDetails} from './product-details';
import {ProductErrorFallback} from './product-error-fallback';

export const routes: Routes = [
{
path: 'product/:id',
component: ProductDetails,
errorComponent: ProductErrorFallback,
},
];
```

To learn more about configuring error fallbacks and retrying failed routes, see the [Error boundaries guide](/guide/routing/error-boundaries).

## Nested Routes

Nested routes, also known as child routes, are a common technique for managing more complex navigation routes where a component has a sub-view that changes based on the URL.
Expand Down
181 changes: 181 additions & 0 deletions adev/src/content/guide/routing/error-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Error boundaries in routing

IMPORTANT: Router error boundaries are in [developer preview](reference/releases#developer-preview).

When building web applications, runtime errors can occur during component initialization, data loading, or rendering. Angular Router allows you to isolate these failures to individual routes using **error boundaries**, preventing a single failing component from crashing the entire application shell.

NOTE: This guide covers error handling at the route level. For catching errors within specific sections of a component's template, refer to the [template `@boundary` block guide](/guide/templates/error-boundaries).

## What are router error boundaries?

Router error boundaries provide declarative error handling at the route outlet level. When a component within a route throws an error—such as a failed network resource, an unexpected null reference in a template, or an error during dependency injection—Angular Router catches the error and displays a dedicated fallback component (`errorComponent`) in place of the failing view.

Sibling outlets, parent layout components, and global navigation headers remain fully interactive and intact.

## Configure an error component

To display a custom fallback UI when a route fails, add the `errorComponent` property to your route definition:

```ts {header:"app.routes.ts"}
import {Routes} from '@angular/router';
import {ProductDetails} from './product-details';
import {ProductErrorFallback} from './product-error-fallback';

export const routes: Routes = [
{
path: 'products/:id',
component: ProductDetails,
errorComponent: ProductErrorFallback,
},
];
```

When you navigate to `/products/123`, if `ProductDetails` throws an error during creation or rendering, Angular Router instantiates `ProductErrorFallback` inside the `<router-outlet>` instead.

NOTE: Unlike route components which can be lazily loaded using `loadComponent`, the `errorComponent` must be eagerly loaded in your route configuration.

---

## Create an error fallback component

An error fallback component is a standard Angular component that can receive the details of the caught error.

### Access error details with an input

You can accept the caught error object by declaring an `error` input:

```angular-ts {header:"product-error-fallback.ts"}
import {Component, input} from '@angular/core';

@Component({
selector: 'app-product-error-fallback',
template: `
<div class="error-card">
<h3>Failed to load product</h3>
<p>{{ error()?.message }}</p>
</div>
`,
})
export class ProductErrorFallback {
readonly error = input<Error>();
}
```

NOTE: If your route configuration includes parameters, query parameters, or route data named `error`, the caught runtime `Error` always takes precedence on the `error` input of an error component.

IMPORTANT: If the `errorComponent` itself throws an error during its creation or rendering, the `RouterOutlet` will not catch it again. The exception will bubble out of the outlet to the root `ErrorHandler` (or an enclosing [template `@boundary` block](/guide/templates/error-boundaries)) to prevent infinite error loops.

## Route parameter and data binding

When using `withComponentInputBinding()`, Angular Router binds route parameters and static data to inputs on the `errorComponent` as well. This allows you to display contextual information in your error UI:

```angular-ts {header:"product-error-fallback.ts"}
import {Component, input} from '@angular/core';

@Component({
selector: 'app-product-error-fallback',
template: `
<div class="error-card">
<h3>Failed to load product #{{ id() }}</h3>
<p>{{ error()?.message }}</p>
</div>
`,
})
export class ProductErrorFallback {
readonly id = input<string>();
readonly error = input<Error>();
}
```

## Global error boundary configuration

You can enable router error boundaries globally across your application using the `withErrorBoundaries` feature in `provideRouter`:

```ts {header:"app.config.ts"}
import {ApplicationConfig, inject} from '@angular/core';
import {provideRouter, withComponentInputBinding, withErrorBoundaries} from '@angular/router';
import {routes} from './app.routes';
import {GlobalErrorFallback} from './global-error-fallback';
import {AnalyticsService} from './analytics.service';

export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(),
withErrorBoundaries({
// Default fallback component for routes that do not define their own errorComponent
defaultErrorComponent: GlobalErrorFallback,

// Global telemetry hook invoked whenever a route error is caught
onError: (error, details) => {
inject(AnalyticsService).logException(error, details);
},
}),
),
],
};
```

### Options

| Option | Type | Description |
| ----------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `defaultErrorComponent` | `Type<unknown>` | Default fallback component to render when a route without its own `errorComponent` throws an error. |
| `onError` | `(error: Error, details?: ErrorDetails) => void` | Global callback invoked whenever any error is caught by a router error boundary. Ideal for sending telemetry to monitoring services. |

---

## Redirecting with `RedirectCommand`

When an authentication failure or data condition requires navigating to another page, components and route resources can throw a `RedirectCommand`:

```angular-ts {header:"user-profile.ts"}
import {Component, inject, resource} from '@angular/core';
import {Router, RedirectCommand} from '@angular/router';

@Component({
selector: 'app-user-profile',
template: `...`,
})
export class UserProfile {
private router = inject(Router);

userData = resource({
loader: async () => {
const response = await fetch('/api/user');
if (response.status === 401) {
// Automatically redirects to /login when unauthorized
throw new RedirectCommand(this.router.parseUrl('/login'));
}
return response.json();
},
});
}
```

When a `RedirectCommand` is thrown:

1. `RouterOutlet` catches the command and initiates navigation to the specified redirect destination.
2. If nested outlets or template error boundaries exist, Angular deduplicates the command to ensure the navigation is only triggered once.
3. The error component (if configured) renders during the transition.

NOTE: If a `RedirectCommand` is thrown and no `errorComponent` is configured, the router initiates the navigation and intentionally swallows the error to prevent it from being logged as a crash by the global `ErrorHandler`.

## Error boundary scope vs. navigation errors

It is helpful to understand the distinction between **view-layer errors** and **navigation pipeline errors**:

- **View-layer errors (Error boundaries)**: Occur inside component constructors, dependency injection, lifecycle hooks (`ngOnInit`), template expressions, and component `resource()` loaders. These are caught by `RouterOutlet` and display the `errorComponent`.
- **Navigation pipeline errors (`NavigationError`)**: Occur before route activation, such as failing `canActivate` / `canMatch` guards or route `resolve` data functions. These halt the navigation transition before reaching the outlet and can be customized using `withNavigationErrorHandler`.

NOTE: When an error boundary displays an `errorComponent`, any `canDeactivate` guards configured for that route are bypassed. This ensures users can safely navigate away from the error fallback without triggering guards that expect state from the failed primary component.

## Next steps

<docs-pill-row>
<docs-pill href="/guide/templates/error-boundaries" title="Template error boundaries"/>
<docs-pill href="/guide/routing/show-routes-with-outlets" title="Show routes with outlets"/>
<docs-pill href="/guide/routing/redirecting-routes" title="Redirecting routes"/>
<docs-pill href="/guide/routing/data-fetching-with-resources" title="Data fetching with resources"/>
</docs-pill-row>
31 changes: 31 additions & 0 deletions adev/src/content/guide/routing/redirecting-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,37 @@ export const routes: Routes = [

To learn more, check out [the API docs for the RedirectFunction](api/router/RedirectFunction).

## Redirecting from components and resources with `RedirectCommand`

In addition to static and function-based route redirects, components and route resources can imperatively trigger a redirect by throwing a `RedirectCommand`. This is especially useful for redirecting unauthenticated users or handling dynamic authorization checks during data fetching:

```angular-ts
import {Component, inject, resource} from '@angular/core';
import {Router, RedirectCommand} from '@angular/router';

@Component({
selector: 'app-account',
template: `<p>Account Details</p>`,
})
export class AccountPage {
private router = inject(Router);

accountData = resource({
loader: async () => {
const response = await fetch('/api/account');
if (response.status === 401) {
// Automatically redirects to /login when unauthorized
throw new RedirectCommand(this.router.parseUrl('/login'));
}
return response.json();
},
});
}
```

When a `RedirectCommand` is thrown inside a routed component constructor, lifecycle hook, or route resource loader, the Angular Router intercepts the command and dispatches navigation to the target URL automatically.

## Next steps

For more information about the `redirectTo` property, check out the [API docs](api/router/Route#redirectTo).
To learn more about error boundaries and catching component failures, see the [Error boundaries guide](/guide/routing/error-boundaries).
17 changes: 16 additions & 1 deletion goldens/public-api/router/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DefaultExport } from '@angular/core';
import { ElementRef } from '@angular/core';
import { EnvironmentInjector } from '@angular/core';
import { EnvironmentProviders } from '@angular/core';
import { ErrorDetails } from '@angular/core';
import { EventEmitter } from '@angular/core';
import * as i0 from '@angular/core';
import { InjectionToken } from '@angular/core';
Expand Down Expand Up @@ -270,6 +271,15 @@ export type DisabledInitialNavigationFeature = RouterFeature<RouterFeatureKind.D
// @public
export type EnabledBlockingInitialNavigationFeature = RouterFeature<RouterFeatureKind.EnabledBlockingInitialNavigationFeature>;

// @public
export type ErrorBoundariesFeature = RouterFeature<RouterFeatureKind.ErrorBoundariesFeature>;

// @public
export interface ErrorBoundaryOptions {
defaultErrorComponent?: Type<unknown>;
onError?: (error: Error, details?: ErrorDetails) => void;
}

// @public
type Event_2 = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | GuardsCheckStart | GuardsCheckEnd | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart | ChildActivationEnd | ActivationStart | ActivationEnd | Scroll | ResolveStart | ResolveEnd | NavigationSkipped;
export { Event_2 as Event }
Expand Down Expand Up @@ -689,6 +699,7 @@ export interface Route {
children?: Routes;
component?: Type<any>;
data?: Data;
errorComponent?: Type<unknown>;
loadChildren?: LoadChildren;
loadComponent?: () => Type<unknown> | Observable<Type<unknown> | DefaultExport<Type<unknown>>> | Promise<Type<unknown> | DefaultExport<Type<unknown>>>;
matcher?: UrlMatcher;
Expand Down Expand Up @@ -818,7 +829,7 @@ export interface RouterFeature<FeatureKind extends RouterFeatureKind> {
}

// @public
export type RouterFeatures = PreloadingFeature | DebugTracingFeature | InitialNavigationFeature | InMemoryScrollingFeature | RouterConfigurationFeature | NavigationErrorHandlerFeature | ComponentInputBindingFeature | ViewTransitionsFeature | AutoCleanupInjectorsFeature | RouterHashLocationFeature | ExperimentalPlatformNavigationFeature | RouterResourcesFeature;
export type RouterFeatures = PreloadingFeature | DebugTracingFeature | InitialNavigationFeature | InMemoryScrollingFeature | RouterConfigurationFeature | NavigationErrorHandlerFeature | ComponentInputBindingFeature | ViewTransitionsFeature | AutoCleanupInjectorsFeature | RouterHashLocationFeature | ExperimentalPlatformNavigationFeature | RouterResourcesFeature | ErrorBoundariesFeature;

// @public
export type RouterHashLocationFeature = RouterFeature<RouterFeatureKind.RouterHashLocationFeature>;
Expand Down Expand Up @@ -968,6 +979,7 @@ export interface RouterOutletContract {
detach(): ComponentRef<unknown>;
detachEvents?: EventEmitter<unknown>;
isActivated: boolean;
isErrorComponentActive?: boolean;
readonly supportsBindingToComponentInputs?: true;
}

Expand Down Expand Up @@ -1181,6 +1193,9 @@ export function withDisabledInitialNavigation(): DisabledInitialNavigationFeatur
// @public
export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNavigationFeature;

// @public
export function withErrorBoundaries(options?: ErrorBoundaryOptions): ErrorBoundariesFeature;

// @public @deprecated
export function withExperimentalAutoCleanupInjectors(): AutoCleanupInjectorsFeature;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@
"RENDERER",
"REQUIRED_UNSET_VALUE",
"ROUTER_CONFIGURATION",
"ROUTER_ERROR_BOUNDARY_HANDLER",
"ROUTER_OUTLET_DATA",
"ROUTER_PRELOADER",
"ROUTER_RESOURCES_FEATURE",
Expand Down
Loading
Loading