A guard is a small function that answers a question about navigation: may the user enter, leave, or load this route? You already saw canActivate. Here are the other guards, plus resolve and preloading.
What you will learn
canActivateChildto protect a whole group of routescanDeactivateto warn about unsaved changesresolveto load data before the page showscanMatch, the modern replacement forCanLoad- A custom preloading strategy
Guards are functions now
Modern guards are plain functions. Class-based guards and CanLoad are legacy. Inside a guard you can call inject().
- canActivate: Question it answers: May I enter this route?
- canActivateChild: Question it answers: May I enter any child of this route?
- canDeactivate: Question it answers: May I leave this route?
- canMatch: Question it answers: Should this route even be considered (and its code downloaded)?
- resolve: Question it answers: Give me the data first
A guard returns true, false, a UrlTree (redirect) or an Observable or Promise of those.
One service for the examples
// src/app/guards2/session.ts
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class Session {
readonly user = signal<{ name: string; role: 'admin' | 'member' } | null>(null);
login(role: 'admin' | 'member') {
this.user.set({ name: 'Asha', role });
}
logout() {
this.user.set(null);
}
}
canActivateChild
Put it on a parent route and it runs for every child. One check protects the whole admin area:
// src/app/guards2/guards.ts
import { inject } from '@angular/core';
import { CanActivateChildFn, CanDeactivateFn, CanMatchFn, ResolveFn, Router } from '@angular/router';
import { Session } from './session';
export const adminOnly: CanActivateChildFn = () => {
const user = inject(Session).user();
// returning a UrlTree redirects instead of just blocking
return user?.role === 'admin' ? true : inject(Router).parseUrl('/denied');
};
// canMatch replaces CanLoad: if it fails, the lazy chunk is never downloaded
export const memberOnly: CanMatchFn = () => inject(Session).user() !== null;
// canDeactivate: the component decides if it can be left
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const leaveGuard: CanDeactivateFn<HasUnsavedChanges> = (component) =>
!component.hasUnsavedChanges() || confirm('You have unsaved changes. Leave anyway?');
// resolve: fetch (or compute) data before the component is created
export interface Article {
id: string;
title: string;
}
export const articleResolver: ResolveFn<Article> = (route) => {
const id = route.paramMap.get('id') ?? '0';
return { id, title: 'Article ' + id }; // could also return an Observable from HttpClient
};
Using them in routes
// src/app/guards2/pages.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { Article, HasUnsavedChanges } from './guards';
@Component({ selector: 'app-admin-home', template: '<h2>Admin home</h2>' })
export class AdminHome {}
@Component({ selector: 'app-denied', template: '<p>Access denied</p>' })
export class Denied {}
@Component({
selector: 'app-article',
template: '<h2>{{ article.title }}</h2>',
})
export class ArticlePage {
// resolved data is stored under the key used in the route's resolve object
article = inject(ActivatedRoute).snapshot.data['article'] as Article;
}
@Component({
selector: 'app-editor',
imports: [FormsModule],
template: '<textarea [(ngModel)]="text"></textarea>',
})
export class Editor implements HasUnsavedChanges {
text = '';
saved = '';
hasUnsavedChanges() {
return this.text !== this.saved;
}
}
// src/app/guards2/guards.routes.ts
import { Routes } from '@angular/router';
import { adminOnly, articleResolver, leaveGuard, memberOnly } from './guards';
import { AdminHome, ArticlePage, Denied, Editor } from './pages';
export const GUARD_ROUTES: Routes = [
{
path: 'admin',
canActivateChild: [adminOnly],
children: [{ path: '', component: AdminHome }],
},
{ path: 'denied', component: Denied },
{ path: 'editor', component: Editor, canDeactivate: [leaveGuard] },
{ path: 'articles/:id', component: ArticlePage, resolve: { article: articleResolver } },
{
path: 'members',
canMatch: [memberOnly],
loadComponent: () => import('./pages').then((m) => m.AdminHome),
},
{ path: 'members', component: Denied }, // used when canMatch says no
];
Notes:
- Read resolved data from
route.data(orsnapshot.data). If you enablewithComponentInputBinding()inprovideRouter, it also arrives as an input with the same name. canMatchcan be used twice for the same path. The first route that matches wins, so you can show a different page to logged in and logged out users.- A resolver that is slow delays navigation. Show a loading bar using router events.
CanLoad is legacy
CanLoad (a class interface) only decided whether to download a lazy module. canMatch does the same and more, so use canMatch.
Custom preloading strategy
After the first screen, Angular can download lazy routes in the background. PreloadAllModules loads everything. A custom strategy lets you choose, using route data:
// src/app/guards2/selective-preload.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SelectivePreload implements PreloadingStrategy {
preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
return route.data?.['preload'] ? load() : of(null);
}
}
// File: register it
provideRouter(routes, withPreloading(SelectivePreload));
// and mark the routes you want early:
{ path: 'reports', loadComponent: ..., data: { preload: true } }
Test it
// src/app/guards2/guards.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideRouter, Router } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
import { GUARD_ROUTES } from './guards.routes';
import { Session } from './session';
describe('guards', () => {
beforeEach(() => TestBed.configureTestingModule({ providers: [provideRouter(GUARD_ROUTES)] }));
it('redirects non admins away from the admin area', async () => {
TestBed.inject(Session).login('member');
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/admin');
expect(TestBed.inject(Router).url).toBe('/denied');
});
it('lets admins in', async () => {
TestBed.inject(Session).login('admin');
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/admin');
expect(TestBed.inject(Router).url).toBe('/admin');
});
it('delivers resolved data', async () => {
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/articles/5');
expect((harness.routeNativeElement as HTMLElement).textContent).toContain('Article 5');
});
it('blocks leaving an editor with unsaved text when the user cancels', async () => {
vi.spyOn(window, 'confirm').mockReturnValue(false);
const harness = await RouterTestingHarness.create();
const editor = await harness.navigateByUrl('/editor');
(editor as unknown as { text: string }).text = 'draft';
await harness.navigateByUrl('/denied');
expect(TestBed.inject(Router).url).toBe('/editor');
});
it('uses canMatch to pick a route', async () => {
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/members');
expect((harness.routeNativeElement as HTMLElement).textContent).toContain('Access denied');
});
});
Common mistakes
- Treating guards as security. They only improve the experience. The server must check permissions again.
- Using
CanLoadclasses in new code. - Doing slow work in a resolver without any loading feedback.
- Forgetting that a
canDeactivateguard only runs for navigation inside the app. For closing the tab use thebeforeunloadevent.
Try it yourself
- Add a
canDeactivateguard to a reactive form that checksform.dirty. - Mark one lazy route with
data: { preload: true }and see it load early in the network tab. - Replace the
confirm()with your own dialog by returning an Observable<boolean>.
Quick check
- Which guard replaces
CanLoad? - What does returning a
UrlTreedo? - Why can a resolver feel slow?
Next, continue with Angular Lazy Loading: Load Routes and Components on Demand.