Skip to content
Angular ng routing 4 min read

Route Resolvers

A route resolver lets you fetch the data a component needs before the router activates the route, so the component renders with everything already in place. Instead of showing an empty shell and a spinner while a service call resolves inside ngOnInit, the navigation pauses until the data arrives, then the destination component appears fully populated. In modern Angular this is expressed with a simple functional resolver — a plain function typed as ResolveFn<T> — rather than a class implementing the legacy Resolve interface.

What a resolver is

A resolver is a function the router calls during navigation. It returns the data (synchronously, or as an Observable or Promise), and the router waits for it to complete before activating the route. The resolved value is attached to the ActivatedRoute snapshot under the data key you choose, where the target component can read it.

Resolvers shine when a component is meaningless without its data — a product detail page, an editable form pre-loaded from the server, or a dashboard whose layout depends on the payload. They also keep components lean: the component reads ready-made data instead of orchestrating loading state itself.

Writing a functional resolver

A functional resolver is a function assigned to ResolveFn<T>. It receives the activated route snapshot and router state, and can use inject() to grab services. The T type parameter is the shape of the data it produces.

// product.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { Observable } from 'rxjs';
import { ProductService } from './product.service';
import { Product } from './product.model';

export const productResolver: ResolveFn<Product> = (route): Observable<Product> => {
  const productService = inject(ProductService);
  const id = route.paramMap.get('id')!;
  return productService.getById(id);
};

You then attach it to a route with the resolve map. Each key in the map becomes a property on the route’s data.

// app.routes.ts
import { Routes } from '@angular/router';
import { productResolver } from './product.resolver';
import { ProductDetailComponent } from './product-detail.component';

export const routes: Routes = [
  {
    path: 'products/:id',
    component: ProductDetailComponent,
    resolve: { product: productResolver },
  },
];

Reading resolved data in a component

The router stores resolved values on the ActivatedRoute. The cleanest modern approach is to enable withComponentInputBinding() so resolved keys are bound directly to @Input() (or signal input()) properties matching the key name.

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes, withComponentInputBinding())],
};
// product-detail.component.ts
import { Component, input } from '@angular/core';
import { Product } from './product.model';

@Component({
  selector: 'app-product-detail',
  standalone: true,
  template: `
    @if (product(); as p) {
      <h1>{{ p.name }}</h1>
      <p class="price">{{ p.price | currency }}</p>
    }
  `,
})
export class ProductDetailComponent {
  // Bound from resolve: { product: ... } via withComponentInputBinding()
  product = input.required<Product>();
}

Alternatively, read the data reactively from the route. Use route.data as a signal via toSignal, or subscribe to the observable:

import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';

@Component({ /* ... */ })
export class ProductDetailComponent {
  private route = inject(ActivatedRoute);
  product = toSignal(this.route.data.pipe(map((d) => d['product'])));
}

Returning different value types

A resolver can return a plain value, an Observable, or a Promise. The router unwraps observables and promises and only completes once they emit/resolve.

Return typeWhen to useRouter behaviour
Plain value (T)Data already in memory / computedActivates immediately
Observable<T>Service call via HttpClientWaits for first emission, then completes
Promise<T>async/await data sourcesWaits for the promise to resolve

The router takes the first value an observable emits and then treats the resolve as done — it does not keep the subscription open. If you need a stream that keeps updating, fetch the initial value in the resolver and subscribe to live updates inside the component.

Handling errors and cancellation

If a resolver’s observable errors, the navigation is cancelled and the route never activates. Catch errors inside the resolver to redirect or return a fallback so the user is not stranded on the previous page.

export const productResolver: ResolveFn<Product | null> = (route) => {
  const products = inject(ProductService);
  const router = inject(Router);
  const id = route.paramMap.get('id')!;

  return products.getById(id).pipe(
    catchError(() => {
      router.navigate(['/products']);
      return of(null);
    })
  );
};

Output:

Navigation to /products/42 fails -> resolver catches 404 -> redirect to /products

Because navigation pauses while a resolver runs, listen for the router’s ResolveStart / ResolveEnd events (or the broader NavigationStart/NavigationEnd) to drive a global loading bar during the wait.

Best practices

  • Keep resolvers focused on a single fetch; compose multiple keys in the resolve map rather than building one giant resolver.
  • Always handle errors with catchError and a redirect or fallback, so a failed fetch doesn’t silently abandon navigation.
  • Use withComponentInputBinding() to bind resolved data straight to input() signals — it removes boilerplate ActivatedRoute plumbing.
  • Don’t put slow, optional, or “nice to have” data in a resolver; it blocks the whole navigation. Load that lazily inside the component instead.
  • Type every resolver with ResolveFn<T> so the resolved value and component input stay in sync.
  • Remember resolvers run on every navigation to the route — leverage caching in the service if the same data is requested repeatedly.
Last updated June 14, 2026
Was this helpful?