AngularJSRoute Resolvers

Route Resolvers in Angular

Route resolvers pre-fetch data before a route activates, so the component renders with data already available — no loading spinners inside the component and no empty-state flicker.

A resolver is a service (or a plain function in modern Angular) that implements the ResolveFn<T> type. The router calls it, waits for the Observable/Promise to complete, and only then navigates to the target route.

Why Use Resolvers?

Without a resolver the typical flow is:

  1. Navigate to the route
  2. Component renders with an empty/undefined model
  3. Component calls a service in ngOnInit
  4. Template shows a spinner until data arrives

With a resolver:

  1. Router calls the resolver
  2. Resolver fetches data from the API
  3. Navigation completes with data in ActivatedRoute.data
  4. Component renders immediately with real data

Without Resolver

With Resolver

Component manages loading state

Data arrives before component renders

Template needs null-guards

Data is guaranteed on init

Loading spinner inside component

Navigation-level loading indicator

Complex ngOnInit logic

Simple route.data access

Creating a Functional Resolver (Modern Angular 14+)

Since Angular 14 you can use a plain ResolveFn instead of a class-based resolver. This is the recommended approach.

TS
// src/app/resolvers/product.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product.model';

export const productResolver: ResolveFn<Product> = (route, state) => {
  const productService = inject(ProductService);
  const id = route.paramMap.get('id')!;
  return productService.getProduct(+id);
};
Note
Functional resolvers use Angular's inject() function to access services — no constructor injection needed.
Registering the Resolver on a Route

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

export const routes: Routes = [
  {
    path: 'products/:id',
    component: ProductDetailComponent,
    resolve: {
      product: productResolver, // key becomes available as route.data['product']
    },
  },
];
Reading Resolved Data in the Component

TS
// product-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Product } from '../models/product.model';
import { CurrencyPipe } from '@angular/common';

@Component({
  selector: 'app-product-detail',
  standalone: true,
  imports: [CurrencyPipe],
  template: `
    <h1>{{ product.name }}</h1>
    <p>{{ product.description }}</p>
    <p>Price: {{ product.price | currency }}</p>
  `,
})
export class ProductDetailComponent implements OnInit {
  product!: Product;

  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    // Data is already resolved — no async loading needed
    this.product = this.route.snapshot.data['product'];
  }
}
Tip
Use this.route.data (Observable) instead of snapshot.data if the same component instance can be reused across different route params without being destroyed and re-created.
Class-Based Resolver (Angular 12–13 Style)

Before Angular 14, resolvers were class-based services implementing the Resolve interface. You may still encounter these in existing codebases.

TS
// src/app/resolvers/product-class.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product.model';

@Injectable({ providedIn: 'root' })
export class ProductClassResolver implements Resolve<Product> {
  constructor(private productService: ProductService) {}

  resolve(route: ActivatedRouteSnapshot): Observable<Product> {
    const id = route.paramMap.get('id')!;
    return this.productService.getProduct(+id);
  }
}

// In routes array:
// resolve: { product: ProductClassResolver }
Error Handling in Resolvers

If the resolver throws or the Observable errors, navigation is cancelled by default. Use catchError to redirect or provide a fallback.

TS
// src/app/resolvers/product-safe.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn, Router } from '@angular/router';
import { catchError, EMPTY } from 'rxjs';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product.model';

export const productSafeResolver: ResolveFn<Product> = (route) => {
  const productService = inject(ProductService);
  const router = inject(Router);
  const id = route.paramMap.get('id')!;

  return productService.getProduct(+id).pipe(
    catchError((err) => {
      console.error('Product not found', err);
      router.navigate(['/not-found']);
      return EMPTY; // cancel navigation silently
    })
  );
};
Warning
Returning EMPTY cancels the navigation entirely. Return a fallback value (e.g., a default Product object) if you want navigation to proceed despite the error.
Multiple Resolvers on a Single Route

TS
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { userResolver } from './resolvers/user.resolver';
import { productsResolver } from './resolvers/products.resolver';

export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    resolve: {
      user: userResolver,
      products: productsResolver, // both fetched in parallel!
    },
  },
];

// dashboard.component.ts
ngOnInit(): void {
  const data = this.route.snapshot.data;
  this.user = data['user'];
  this.products = data['products'];
}
Note
Angular fires all resolvers for a route in parallel. The route activates only when every resolver completes.
Global Loading Indicator with Router Events

Since resolution happens at the router level, listen to RouterEvents to show a global progress bar while resolvers are running.

TS
// app.component.ts
import { Component } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { RouterOutlet } from '@angular/router';
import { NgIf } from '@angular/common';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, NgIf],
  template: `
    <div *ngIf="loading" class="global-spinner">Loading...</div>
    <router-outlet />
  `,
})
export class AppComponent {
  loading = false;

  constructor(private router: Router) {
    this.router.events.subscribe((event) => {
      if (event instanceof NavigationStart) {
        this.loading = true;
      } else if (
        event instanceof NavigationEnd ||
        event instanceof NavigationCancel ||
        event instanceof NavigationError
      ) {
        this.loading = false;
      }
    });
  }
}
Resolver with Signal-Based Components (Angular 17+)

TS
// product-signal.component.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-product-signal',
  standalone: true,
  template: `
    @if (product(); as p) {
      <h1>{{ p.name }}</h1>
      <p>{{ p.description }}</p>
    }
  `,
})
export class ProductSignalComponent {
  private route = inject(ActivatedRoute);

  product = toSignal(
    this.route.data.pipe(map((data) => data['product']))
  );
}
Best Practices
  • Prefer functional ResolveFn over class-based resolvers for new Angular 14+ code

  • Always handle errors with catchError — never let a resolver silently kill navigation

  • Keep resolvers thin: delegate real logic to injectable services

  • Use EMPTY to cancel navigation on 404; return a fallback value to allow navigation

  • Multiple resolvers on one route run in parallel — split independent data fetches

  • Use NavigationStart/NavigationEnd events for a global loading indicator

  • Avoid heavy computation in resolvers — they are for data fetching only

Summary

Route resolvers guarantee data availability before a component renders. Modern Angular favors the functional ResolveFn API with inject(), making resolvers lightweight and easy to unit-test. Pair them with a global loading indicator driven by router events for a polished navigation experience.