Angular Resolvers Explained — Preload Data Before Navigation
In this tutorial, you will learn about Angular Resolvers Explained. We cover key concepts, practical examples, and best practices to help you master this topic.
Angular resolvers pre-fetch data before a route activates, ensuring the component has the data it needs immediately when it renders, avoiding empty states and loading spinners.
What You'll Learn
- What resolvers are and when to use them
- How to create a resolver function
- How to access resolved data in components
- How to handle resolver errors
- How to combine multiple resolvers
Why It Matters
Resolvers improve user experience by eliminating flickering loading states. When navigating to a user profile, the profile data loads before the component renders. This also simplifies components because they do not need loading state logic.
Real-World Use
Durga Antivirus Pro's scan report page uses a resolver to fetch the full scan data, threat details, and system info before the page renders. The component receives all data as a single observable and can focus on displaying it.
flowchart TD
A[User Navigates] --> B[Resolver]
B --> C[API Call]
C --> D{Success?}
D -->|Yes| E[Route Activates]
D -->|No| F[Error Handler]
E --> G[Component Receives Data]
style A fill:#f97316,color:#fff
Creating a Resolver
A resolver is a function that returns data before the route activates:
import { ResolveFn, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
import { inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable, of } from "rxjs";
import { catchError } from "rxjs/operators";
export interface User {
id: number;
name: string;
email: string;
avatar: string;
role: string;
}
export const userResolver: ResolveFn<User | null> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<User | null> => {
const http = inject(HttpClient);
const userId = route.paramMap.get("id");
return http.get<User>(`https://jsonplaceholder.typicode.com/users/${userId}`).pipe(
catchError(error => {
console.error("Failed to load user:", error);
return of(null);
})
);
};
Route configuration:
import { Routes } from "@angular/router";
import { userResolver } from "./resolvers/user.resolver";
export const routes: Routes = [
{
path: "users/:id",
loadComponent: () => import("./user-detail/user-detail.component").then(m => m.UserDetailComponent),
resolve: {
user: userResolver
}
}
];
Expected output: When navigating to /users/1, the resolver fetches the user data. Only when the data arrives does the UserDetailComponent render.
The resolver key (user) matches the property name in the component's ActivatedRoute.data observable. You can define multiple resolvers with different keys.
Accessing Resolved Data
The component retrieves data from ActivatedRoute:
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { CommonModule } from "@angular/common";
import { User } from "../resolvers/user.resolver";
import { switchMap } from "rxjs";
@Component({
selector: "app-user-detail",
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<p>Role: {{ user.role }}</p>
</div>
<div *ngIf="!user">
<p>User not found or failed to load.</p>
</div>
`
})
export class UserDetailComponent implements OnInit {
user: User | null = null;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.data.subscribe(data => {
this.user = data["user"];
});
}
}
Expected output: The component immediately displays user data or a "User not found" message, without any loading state.
The ActivatedRoute.data observable emits an object with the resolver's return value under the key specified in the route configuration. Since the resolver completes before navigation, the data is available synchronously.
Multiple Resolvers
Combine multiple data sources:
import { ResolveFn } from "@angular/router";
import { inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { forkJoin, Observable, of } from "rxjs";
import { catchError } from "rxjs/operators";
export interface DashboardData {
userStats: any;
recentActivity: any[];
notifications: any[];
}
export const dashboardResolver: ResolveFn<DashboardData> = () => {
const http = inject(HttpClient);
const userStats$ = http.get("/api/dashboard/stats").pipe(catchError(() => of(null)));
const recentActivity$ = http.get("/api/dashboard/activity").pipe(catchError(() => of([])));
const notifications$ = http.get("/api/dashboard/notifications").pipe(catchError(() => of([])));
return forkJoin({
userStats: userStats$,
recentActivity: recentActivity$,
notifications: notifications$
});
};
Route config:
{
path: "dashboard",
component: DashboardComponent,
resolve: {
dashboard: dashboardResolver
}
}
Expected output: All three API calls complete before the dashboard renders. The component receives the combined data object.
forkJoin waits for all observables to complete and emits the last values from each. If any request fails, the catchError provides a fallback value so the resolver does not block navigation entirely.
Error Handling in Resolvers
Handle errors gracefully without blocking navigation:
import { ResolveFn, Router } from "@angular/router";
import { inject } from "@angular/core";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Observable, of, throwError } from "rxjs";
import { catchError, map } from "rxjs/operators";
export const safeResolver: ResolveFn<any> = (route, state) => {
const http = inject(HttpClient);
const router = inject(Router);
const id = route.paramMap.get("id");
return http.get(`/api/items/${id}`).pipe(
map(response => ({ success: true, data: response })),
catchError((error: HttpErrorResponse) => {
if (error.status === 404) {
router.navigate(["/not-found"]);
return of({ success: false, data: null });
}
if (error.status >= 500) {
router.navigate(["/server-error"]);
return of({ success: false, data: null });
}
return of({ success: false, data: null, error: error.message });
})
);
};
Expected output: 404 errors redirect to a not-found page. Server errors show a friendly error page. The component always receives a predictable response structure.
The resolver returns a success-or-fail object that the component can check. For critical errors, the router navigates to dedicated error pages instead of rendering the component with missing data.
When Not to Use Resolvers
Resolvers are not always the right choice:
// BAD: Using resolver for real-time data that changes frequently
export const stockPriceResolver: ResolveFn<number> = () => {
return inject(HttpClient).get<number>("/api/stock-price");
};
// GOOD: Using resolver for static reference data loaded once
export const countryListResolver: ResolveFn<string[]> = () => {
return inject(HttpClient).get<string[]>("/api/countries");
};
// GOOD: Component handles real-time updates itself
@Component({
template: `
<div *ngIf="loading">Loading latest prices...</div>
<div *ngIf="price">{{ price }}</div>
`
})
export class StockPriceComponent implements OnInit {
loading = true;
price = 0;
ngOnInit() {
this.stockService.getPriceStream().subscribe(price => {
this.price = price;
this.loading = false;
});
}
}
Expected output: The stock price updates in real-time after initial navigation, which a resolver cannot support.
Resolvers are best for data that does not change during the component's lifetime: user profiles, article content, configuration data. For real-time data or data that refreshes periodically, load it in the component.
Common Mistakes
Blocking navigation on slow resolvers — If a resolver takes more than a few seconds, the user sees a blank page. Consider loading critical data in the resolver and deferring non-critical data.
Forgetting catchError — A failed resolver that throws blocks navigation entirely. Always catch errors and provide fallback values.
Putting too much data in one resolver — Combine multiple resolvers or resolve only the essential data. Additional data can load lazily in the component.
Not cleaning up subscriptions — Resolvers return observables that complete. If they do not complete, navigation never proceeds.
Using resolvers with route params that change — If only the param changes (not the route), the resolver does not re-run by default. Use
runGuardsAndResolversoption.
Practice Questions
What is the purpose of a resolver? To pre-fetch data before a route activates, ensuring the component has data when it renders.
How do you access resolved data in a component? Through
ActivatedRoute.dataobservable using the key defined in the route configuration.What happens if a resolver errors? If the error is not caught, navigation is blocked. Always use
catchErrorto handle resolver errors.How do you combine multiple API calls in one resolver? Use
forkJoinfrom RxJS to wait for all observables to complete and return a combined object.When should you NOT use a resolver? For real-time or frequently changing data. Load that data directly in the component.
Challenge
Build a ProductPageResolver that fetches product details, related products, and seller info in parallel using forkJoin. Handle each failure individually: if related products fail, show an empty list; if seller info fails, show "Seller info unavailable". The product details are required — if they fail, redirect to a product-not-found page.
FAQ
Mini Project
Build a BlogPostComponent with a resolver that fetches the post content, author details, and comments in parallel. The resolver returns { post, author, comments }. The component shows a loading skeleton composed of CSS-only animation while the resolver loads (simulated with a delay). Handle errors: if the post is not found (404), redirect to a blog listing page. Cache the resolved data in a service so back-navigation does not trigger a re-fetch.
What's Next
Continue with Lazy Loading and SSR:
Angular Lazy Loading, Angular SSR, Angular Guards
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro