Angular 26 🅰️ Route Guards
Routes aren’t always open. Some pages require login, some need a specific role, some should prompt before navigating away with unsaved changes. Route guards are the functions or classes that decide whether navigation proceeds. They run before the route activates (or deactivates), return true or false (or a UrlTree for a redirect), and can be async. Angular ships five guard types: CanActivate, CanActivateChild, CanDeactivate, CanMatch, and CanLoad (deprecated). This chapter covers the modern functional guards — functions that use inject() — and the situations each one fits.
Key point: A guard is a function or class that returns true, false, UrlTree, or a promise/observable of those. true allows navigation, false cancels it, and a UrlTree redirects. Guards run before the route activates, so protected code and lazy chunks are never loaded if the guard denies. Use the functional form for new code — a CanActivateFn that calls inject() is shorter, testable, and fits standalone components.
What a route guard is
A route guard decides whether navigation to (or from) a route should proceed.
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login']);
};
The guard runs when the route is about to activate. It returns true to allow navigation or a UrlTree to redirect.
What a guard does:
- Decides whether navigation proceeds
- Runs before the route activates
- Returns
true,false,UrlTree, or a promise/observable of those - Can be async — the router waits
What a guard doesn’t do:
- Doesn’t load the component (that’s after the guard allows)
- Doesn’t fetch data (that’s a resolver)
- Doesn’t change the current URL by itself
- Doesn’t run on every CD cycle — only on navigation
Where guards attach:
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard]
}
The canActivate array holds the guards for the route.
Why guards matter: Without them, every route is public. Guards let you protect pages, verify roles, prompt for unsaved changes, and redirect as needed — before the route’s code loads.
Why “guard”: It guards the route — like a security guard at a door. It decides who gets in. The name matches the function.
Why functional guards: Class guards required
@Injectableand a class per guard. Functional guards are plain functions — shorter, easier to test, and they fit the standalone pattern. The router calls the function with route and state, and the function usesinject()for dependencies.
The five guard types
Angular has five types, each for a different moment.
| Guard | Runs when | Purpose |
|---|---|---|
CanActivate | Before route activates | Allow/deny navigation to a route |
CanActivateChild | Before child route activates | Protect all children of a parent |
CanDeactivate | Before leaving a route | Prompt for unsaved changes |
CanMatch | Before matching a route | Choose among routes with the same path |
CanLoad | Before loading a lazy module | Deprecated — use CanMatch |
The three you’ll use most:
CanActivate— protect a route from unauthorized usersCanDeactivate— prompt before leaving with unsaved changesCanMatch— route feature flags and A/B tests
The one you’ll rarely use:
CanActivateChild— same asCanActivateon the parent for all children
The deprecated one:
CanLoad— replaced byCanMatch
Why five types: Each fires at a different point in navigation. CanActivate decides whether to enter a route. CanDeactivate decides whether to leave. CanMatch decides whether the route even matches a URL. They’re separate because navigation has multiple decision points.
Why CanActivateChild exists: When you have nested routes, you often want the parent to guard all its children. Instead of repeating canActivate on every child, you use canActivateChild on the parent. Same behavior, less repetition.
Why
CanLoadis deprecated: It only worked for lazy modules.CanMatchworks for any route — lazy or not — and gives more control. The Angular team recommendsCanMatchfor new code.
CanActivate — protect a route
CanActivate runs before a route activates.
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
What the guard receives:
route: ActivatedRouteSnapshot— the route being activatedstate: RouterStateSnapshot— the full router state
What it returns:
true— allow navigationfalse— cancel navigationUrlTree— cancel and redirect- Promise or Observable of any of the above
Attaching to a route:
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard]
}
Multiple guards:
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard, roleGuard, featureFlagGuard]
}
All must return true (or a UrlTree that triggers redirect). If any returns false, navigation cancels.
Async guard:
export const authGuard: CanActivateFn = async () => {
const auth = inject(AuthService);
const user = await auth.checkSession();
return user !== null;
};
The router waits for the promise. Returning false cancels navigation.
Observable guard:
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.user$.pipe(map(user => user !== null));
};
The router subscribes and uses the first emission.
Why redirect with UrlTree: Returning a UrlTree cancels navigation and starts a new one. The createUrlTree call builds the redirect target — path plus query params.
Why include returnUrl: So the login page can send the user back to where they were trying to go. state.url is the URL of the attempted navigation.
Why the return type: CanActivateFn returns boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree>. Any of those work.
Why guards run before the component: The route’s component and any lazy chunks are loaded only after the guard allows. That means denied routes never download their code — a nice efficiency property. It also means the component doesn’t exist when the guard runs, so guards can’t rely on component state.
CanActivateChild — protect children
CanActivateChild runs for every child route of a parent.
export const adminGuard: CanActivateChildFn = (childRoute, state) => {
const auth = inject(AuthService);
return auth.hasRole('admin');
};
Attaching to a parent:
{
path: 'admin',
component: AdminComponent,
canActivateChild: [adminGuard],
children: [
{ path: 'users', component: UsersComponent },
{ path: 'settings', component: SettingsComponent }
]
}
Every navigation to /admin/users or /admin/settings runs adminGuard.
The difference from CanActivate: CanActivate runs when the parent activates (once). CanActivateChild runs on every child activation, even if the parent is already active.
When to use it: When a parent owns a set of children that all need the same protection. Writing canActivate: [adminGuard] on every child would duplicate.
Combining with CanActivate: You can use both — CanActivate for the parent itself, CanActivateChild for the children.
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard],
canActivateChild: [adminGuard],
children: [...]
}
The parent requires login; the children require admin.
Why CanActivateChild matters: It centralizes protection for a set of routes. If the parent’s guard changes, all children update. Adding a new child inherits the protection automatically.
Why the childRoute parameter: The guard receives the child being activated, not the parent. It can inspect the child’s params and data.
Why the two are separate:
CanActivatefires when the parent activates — but if you navigate from/admin/usersto/admin/settings, the parent stays active.CanActivatedoesn’t re-fire.CanActivateChilddoes — because a new child is activating. That’s the distinction.
CanDeactivate — prevent leaving
CanDeactivate runs before leaving a route. It’s used to prompt for unsaved changes.
import { CanDeactivateFn } from '@angular/router';
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
if (!component.hasUnsavedChanges()) {
return true;
}
return confirm('You have unsaved changes. Leave anyway?');
};
The guard receives the component instance as its first argument. It calls a method on the component to check for unsaved changes.
The component’s contract:
@Component({ /* ... */ })
export class FormComponent implements HasUnsavedChanges {
private dirty = false;
hasUnsavedChanges(): boolean {
return this.dirty;
}
}
The interface defines the shape the guard expects. The component provides the method.
Attaching to a route:
{
path: 'edit',
component: FormComponent,
canDeactivate: [unsavedChangesGuard]
}
Async version:
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = async (component) => {
if (!component.hasUnsavedChanges()) return true;
// Show a modal that returns a promise
return await confirmDialog('Discard changes?');
};
The router waits for the promise. Returning false cancels navigation.
Why the component parameter: CanDeactivate needs to inspect the component that’s leaving. The generic type CanDeactivateFn<T> tells TypeScript what shape to expect.
Why CanDeactivate is special: It’s the only guard that receives the component. CanActivate and CanMatch receive the route; CanDeactivate needs to ask the component itself.
Common use cases:
- Unsaved form data
- Confirmation dialogs
- Auto-saving before leaving
- Cleanup
Why the component contract: The guard can’t know what “unsaved changes” means for every component. The component exposes a method, and the guard calls it. The contract is the interface.
Why guards can’t inspect component state directly: The component may not exist yet (for
CanActivate) or may be about to be destroyed (forCanDeactivate). The guard receives a reference and calls methods on it. That’s why the component must implement a known interface.
CanMatch — choose routes
CanMatch runs during route matching. It decides whether a route should even match the URL.
import { CanMatchFn } from '@angular/router';
import { inject } from '@angular/core';
import { FeatureFlagService } from './feature-flag.service';
export const featureFlagGuard: CanMatchFn = (route, segments) => {
const flags = inject(FeatureFlagService);
return flags.isEnabled('new-dashboard');
};
The use case — two routes, same path:
export const routes: Routes = [
{
path: 'dashboard',
canMatch: [featureFlagGuard],
loadComponent: () => import('./new-dashboard.component').then(m => m.NewDashboardComponent)
},
{
path: 'dashboard',
loadComponent: () => import('./old-dashboard.component').then(m => m.OldDashboardComponent)
}
];
Both routes have path: 'dashboard'. The router tries them in order. If featureFlagGuard returns true, the new dashboard loads; otherwise, the router tries the next route and falls through to the old dashboard.
What CanMatch receives:
route: Route— the route config (not the snapshot)segments: UrlSegment[]— the URL segments being matched
What it returns: true to use this route, false to try the next one.
The fall-through behavior: Unlike CanActivate (which cancels navigation), CanMatch returning false tells the router to try the next route. If none match, navigation fails.
Common use cases:
- Feature flags — choose between old and new implementations
- A/B tests — route different users to different components
- Permissions — choose among routes based on role
Why CanMatch replaced CanLoad: CanLoad only worked for lazy modules and only returned boolean. CanMatch works for any route, returns boolean | UrlTree, and can be used for fall-through selection. It’s strictly more capable.
Why it’s subtle: CanMatch doesn’t “deny” navigation — it “declines to match.” If no route matches, then navigation fails. The distinction matters: with CanMatch, you always have a fallback route if you want one.
Why CanMatch is at the matching stage: Route matching happens before activation.
CanMatchdecides whether a route is a candidate. If it declines, the router moves on to the next candidate. If none match, the URL doesn’t resolve — that’s when the wildcard route or a 404 fires.
Composing guards
Guards can be combined — all must return truthy.
Multiple guards on one route:
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard, roleGuard, flagGuard]
}
The router runs them in order. The first one to return false or a UrlTree stops the chain.
Combining CanActivate and CanActivateChild:
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard],
canActivateChild: [adminGuard],
children: [
{ path: 'users', component: UsersComponent },
{ path: 'logs', component: LogsComponent }
]
}
The parent requires login; the children require admin.
Creating a guard that composes others:
export function allOf(...guards: CanActivateFn[]): CanActivateFn {
return (route, state) => {
for (const guard of guards) {
const result = guard(route, state);
if (result !== true) return result;
}
return true;
};
}
export const adminGuard = allOf(authGuard, roleGuard, flagGuard);
A composed guard — all must pass.
Composing with inject:
export const requireRole = (role: string): CanActivateFn => {
return () => {
const auth = inject(AuthService);
return auth.hasRole(role);
};
};
export const adminGuard = requireRole('admin');
export const userGuard = requireRole('user');
Guards can be parameterized. The factory returns a CanActivateFn that closes over the role.
Why composition matters: Real apps need “logged in AND has role AND feature is enabled.” Composing guards expresses that cleanly. Each guard does one job; the composition combines them.
Why parameterized guards: A role guard is useful for many roles. A factory function returns a guard configured for a specific role. That’s cleaner than duplicating the guard for each role.
Why multiple guards run in order: The router runs the guards in the array order. It stops on the first failure. That lets you put cheap guards first — login check before role check, role check before feature flag.
A full example
An app with authentication, role, and unsaved-changes guards.
// ============================================
// AUTH SERVICE
// ============================================
import { Injectable, signal } from '@angular/core';
export interface User {
id: number;
name: string;
role: 'admin' | 'user';
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private _user = signal<User | null>(null);
user = this._user.asReadonly();
isLoggedIn(): boolean {
return this._user() !== null;
}
hasRole(role: User['role']): boolean {
return this._user()?.role === role;
}
login(user: User): void {
this._user.set(user);
}
logout(): void {
this._user.set(null);
}
}
// ============================================
// FEATURE FLAG SERVICE
// ============================================
@Injectable({ providedIn: 'root' })
export class FeatureFlagService {
private flags = new Set(['new-dashboard']);
isEnabled(flag: string): boolean {
return this.flags.has(flag);
}
}
// ============================================
// GUARDS
// ============================================
import { inject } from '@angular/core';
import { CanActivateFn, CanActivateChildFn, CanDeactivateFn, CanMatchFn, Router } from '@angular/router';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) return true;
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
export const adminGuard: CanActivateChildFn = () => {
return inject(AuthService).hasRole('admin');
};
export const featureFlagGuard: CanMatchFn = () => {
return inject(FeatureFlagService).isEnabled('new-dashboard');
};
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
if (!component.hasUnsavedChanges()) return true;
return confirm('Discard unsaved changes?');
};
// ============================================
// COMPONENTS
// ============================================
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-login',
standalone: true,
template: `<h1>Login</h1>`
})
export class LoginComponent {}
@Component({
selector: 'app-admin',
standalone: true,
template: `<h1>Admin</h1><router-outlet></router-outlet>`
})
export class AdminComponent {}
@Component({
selector: 'app-dashboard',
standalone: true,
template: `<h1>New Dashboard</h1>`
})
export class NewDashboardComponent {}
@Component({
selector: 'app-old-dashboard',
standalone: true,
template: `<h1>Old Dashboard</h1>`
})
export class OldDashboardComponent {}
@Component({
selector: 'app-edit-form',
standalone: true,
template: `<h1>Edit Form</h1>`
})
export class EditFormComponent implements HasUnsavedChanges {
dirty = signal(false);
hasUnsavedChanges(): boolean {
return this.dirty();
}
}
// ============================================
// ROUTES
// ============================================
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'login',
component: LoginComponent
},
{
path: 'dashboard',
canMatch: [featureFlagGuard],
loadComponent: () => import('./new-dashboard.component').then(m => m.NewDashboardComponent)
},
{
path: 'dashboard',
loadComponent: () => import('./old-dashboard.component').then(m => m.OldDashboardComponent)
},
{
path: 'admin',
canActivate: [authGuard],
canActivateChild: [adminGuard],
loadComponent: () => import('./admin.component').then(m => m.AdminComponent),
children: [
{ path: 'users', component: AdminComponent },
{ path: 'settings', component: AdminComponent }
]
},
{
path: 'edit',
canActivate: [authGuard],
canDeactivate: [unsavedChangesGuard],
component: EditFormComponent
}
];
What this shows:
authGuard— requires login, redirects to/loginwithreturnUrladminGuard— asCanActivateChild, protects every child of/adminfeatureFlagGuard— asCanMatch, chooses the new dashboard when the flag is onunsavedChangesGuard— asCanDeactivate, prompts before leaving/edit- Composition —
authGuardon/adminitself,adminGuardon its children
Each guard does one job. Together they enforce the app’s access rules.
Why this shape: It’s a realistic app. Login protection, role-based access, feature flags, unsaved prompts — the four common cases. Every guard uses the functional form. The routes compose them with arrays.
Complete Example Session
# ============================================
# PART 1: BASIC CANACTIVATE
# ============================================
cat > auth.guard.ts << 'EOF'
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
export const authGuard: CanActivateFn = () => {
const router = inject(Router);
const isLoggedIn = true; // stub
if (isLoggedIn) return true;
return router.createUrlTree(['/login']);
};
EOF
npx tsc --noEmit auth.guard.ts
# (no errors)
# ============================================
# PART 2: PARAMETERIZED GUARD
# ============================================
cat > role.guard.ts << 'EOF'
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export function requireRole(role: string): CanActivateFn {
return () => {
return inject(AuthService).hasRole(role);
};
}
export const adminGuard = requireRole('admin');
export const userGuard = requireRole('user');
EOF
npx tsc --noEmit role.guard.ts
# (no errors)
# ============================================
# PART 3: CANDECTIVATE
# ============================================
cat > unsaved.guard.ts << 'EOF'
import { CanDeactivateFn } from '@angular/router';
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
if (!component.hasUnsavedChanges()) return true;
return confirm('Discard changes?');
};
EOF
npx tsc --noEmit unsaved.guard.ts
# (no errors)
# ============================================
# PART 4: CANMATCH
# ============================================
cat > flag.guard.ts << 'EOF'
import { CanMatchFn } from '@angular/router';
import { inject } from '@angular/core';
export const flagGuard: CanMatchFn = (route, segments) => {
return inject(FeatureFlagService).isEnabled(route.data?.['flag']);
};
EOF
npx tsc --noEmit flag.guard.ts
# (no errors)
# ============================================
# PART 5: ASYNC GUARD
# ============================================
cat > async.guard.ts << 'EOF'
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
export const asyncGuard: CanActivateFn = async () => {
const result = await Promise.resolve(true);
return result;
};
EOF
npx tsc --noEmit async.guard.ts
# (no errors)
# ============================================
# PART 6: COMPOSITION
# ============================================
cat > compose.ts << 'EOF'
import { CanActivateFn } from '@angular/router';
export function allOf(...guards: CanActivateFn[]): CanActivateFn {
return (route, state) => {
for (const guard of guards) {
const result = guard(route, state);
if (result !== true) return result;
}
return true;
};
}
export function anyOf(...guards: CanActivateFn[]): CanActivateFn {
return (route, state) => {
for (const guard of guards) {
const result = guard(route, state);
if (result === true) return true;
}
return false;
};
}
EOF
npx tsc --noEmit compose.ts
# (no errors)
# ============================================
# PART 7: ROUTE CONFIG
# ============================================
cat > routes.ts << 'EOF'
import { Routes } from '@angular/router';
import { authGuard } from './auth.guard';
import { adminGuard } from './role.guard';
import { unsavedGuard } from './unsaved.guard';
export const routes: Routes = [
{ path: 'login', loadComponent: () => import('./login.component').then(m => m.LoginComponent) },
{
path: 'admin',
canActivate: [authGuard],
canActivateChild: [adminGuard],
loadChildren: () => import('./admin.routes').then(m => m.adminRoutes)
},
{
path: 'edit',
canActivate: [authGuard],
canDeactivate: [unsavedGuard],
loadComponent: () => import('./edit.component').then(m => m.EditComponent)
}
];
EOF
npx tsc --noEmit routes.ts
# (no errors)
# ============================================
# PART 8: TESTING A GUARD
# ============================================
cat > auth.guard.spec.ts << 'EOF'
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { authGuard } from './auth.guard';
import { AuthService } from './auth.service';
describe('authGuard', () => {
let auth: jasmine.SpyObj<AuthService>;
let router: jasmine.SpyObj<Router>;
beforeEach(() => {
auth = jasmine.createSpyObj('AuthService', ['isLoggedIn']);
router = jasmine.createSpyObj('Router', ['createUrlTree']);
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useValue: auth },
{ provide: Router, useValue: router }
]
});
});
it('allows when logged in', () => {
auth.isLoggedIn.and.returnValue(true);
const result = TestBed.runInInjectionContext(() =>
authGuard({} as any, {} as any)
);
expect(result).toBe(true);
});
it('redirects when not logged in', () => {
auth.isLoggedIn.and.returnValue(false);
router.createUrlTree.and.returnValue({} as any);
const result = TestBed.runInInjectionContext(() =>
authGuard({} as any, {} as any)
);
expect(router.createUrlTree).toHaveBeenCalled();
});
});
EOF
echo "Test with: ng test"
Quick Reference
The Five Guard Types
| Guard | Runs | Purpose |
|---|---|---|
CanActivate | Before activating | Allow/deny route |
CanActivateChild | Before child activates | Protect children |
CanDeactivate | Before leaving | Prompt for changes |
CanMatch | During matching | Choose route |
CanLoad | Before lazy load | Deprecated |
Functional Types
| Type | Signature |
|---|---|
CanActivateFn | (route, state) => boolean | UrlTree | Promise | Observable |
CanActivateChildFn | Same as CanActivateFn |
CanDeactivateFn<T> | (component, currentRoute, nextState) => ... |
CanMatchFn | (route, segments) => boolean | UrlTree |
Return Values
| Return | Effect |
|---|---|
true | Allow |
false | Deny |
UrlTree | Redirect |
Promise<...> | Async — router waits |
Observable<...> | Async — first emission |
Guard Arguments
| Guard | Receives |
|---|---|
CanActivate | route (snapshot), state |
CanActivateChild | childRoute, state |
CanDeactivate | component, currentRoute, nextState |
CanMatch | route (config), segments |
Attaching Guards
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard],
canActivateChild: [adminGuard],
canDeactivate: [unsavedGuard],
canMatch: [flagGuard]
}
Deprecated CanLoad
// OLD
{ path: 'x', canLoad: [guard], loadChildren: ... }
// NEW
{ path: 'x', canMatch: [guard], loadChildren: ... }
Composing Guards
| Form | Meaning |
|---|---|
canActivate: [a, b, c] | All must pass |
allOf(a, b, c) | Custom all |
anyOf(a, b, c) | Any passes |
Parameterized Guard
function requireRole(role: string): CanActivateFn {
return () => inject(AuthService).hasRole(role);
}
export const adminGuard = requireRole('admin');
Redirect with UrlTree
router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
Async Guard
export const guard: CanActivateFn = async () => {
const user = await auth.checkSession();
return user !== null;
};
Observable Guard
export const guard: CanActivateFn = () => {
return auth.user$.pipe(map(u => u !== null));
};
CanDeactivate Contract
interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const guard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
return !component.hasUnsavedChanges();
};
CanMatch Fall-Through
[
{ path: 'dashboard', canMatch: [newGuard], component: NewDashboard },
{ path: 'dashboard', component: OldDashboard }
]
// Router tries in order; falls through on false
Guard Order
| Order | Guard |
|---|---|
| 1 | CanMatch |
| 2 | CanActivate |
| 3 | CanActivateChild |
| 4 | Resolvers |
| 5 | Activate component |
| 6 | CanDeactivate (on leave) |
Common Errors
| Error | Cause |
|---|---|
Cannot find name 'inject' | Missing import |
inject outside context | Guard called in wrong place |
Guard returned undefined | Missing return |
Function not assignable | Wrong guard type |
Testing Guards
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useValue: mockAuth }
]
});
const result = TestBed.runInInjectionContext(() =>
authGuard({} as any, {} as any)
);
runInInjectionContext provides the injection context inject() needs.
Guard Frequency
| Guard | Frequency |
|---|---|
CanActivate | On activation |
CanActivateChild | On each child activation |
CanDeactivate | On leaving |
CanMatch | On matching |
CanLoad | On lazy load (deprecated) |
Best Practices
✅ Do This:
// Use functional guards
export const authGuard: CanActivateFn = () => { }; // ✅
// Use inject() for dependencies
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.isLoggedIn();
}; // ✅
// Redirect with UrlTree
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
}); // ✅
// Parameterize guards
export function requireRole(role: string): CanActivateFn {
return () => inject(AuthService).hasRole(role);
} // ✅
// Use CanActivateChild for parent-level protection
{ path: 'admin', canActivateChild: [adminGuard], children: [...] } // ✅
// Use CanMatch for feature flags
{ path: 'x', canMatch: [flagGuard], component: NewComponent },
{ path: 'x', component: OldComponent } // ✅
// Use CanDeactivate with an interface
interface HasUnsavedChanges { hasUnsavedChanges(): boolean; }
export const guard: CanDeactivateFn<HasUnsavedChanges> = (c) => !c.hasUnsavedChanges(); // ✅
// Compose guards in arrays
canActivate: [authGuard, roleGuard, flagGuard] // ✅
// Test guards with TestBed.runInInjectionContext
TestBed.runInInjectionContext(() => authGuard(...)); // ✅
❌ Don’t Do This:
// Don't use class-based guards for new code
@Injectable()
class AuthGuard implements CanActivate { } // ⚠️
// Don't forget to return
export const guard: CanActivateFn = () => {
inject(AuthService).isLoggedIn(); // ⚠️ no return // ⚠️
}; // ⚠️
// Don't use CanLoad in new code
{ path: 'x', canLoad: [guard] } // ⚠️ deprecated
// Don't return a promise from CanMatch
export const guard: CanMatchFn = async () => true; // ⚠️ not supported
// Don't skip the guard's return in async functions
export const guard: CanActivateFn = async () => {
await something();
}; // ⚠️ undefined
// Don't put guards on the wrong property
{ path: 'x', canActivateChild: [guard] } // ⚠️ only for parents
// Don't call inject() outside a guard's function body
const auth = inject(AuthService);
export const guard: CanActivateFn = () => true; // ⚠️ wrong place
// Don't forget to test guards
// Unguarded guards let everything through // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| No return | Navigation blocked | Always return a value |
| Async without await | Returns undefined | Return the promise |
inject() outside body | Runtime error | Call inside the function |
CanMatch returns Promise | Not supported | Return boolean or UrlTree |
Forgot canActivateChild | Children unguarded | Add on parent |
CanDeactivate on wrong property | Doesn’t fire | Use canDeactivate |
Missing UrlTree import | Compile error | Import from @angular/router |
| Guards on the wrong level | Wrong behavior | Match to the guard type |
| Testing without context | inject() fails | TestBed.runInInjectionContext |
| Async guard race conditions | Flickering navigation | Return the promise |
Real-World Examples
1. Auth guard
export const authGuard: CanActivateFn = () =>
inject(AuthService).isLoggedIn();
2. Redirect guard
export const guard: CanActivateFn = () => {
const router = inject(Router);
if (isLoggedIn()) return true;
return router.createUrlTree(['/login']);
};
3. Role guard
export const adminGuard: CanActivateFn = () =>
inject(AuthService).hasRole('admin');
4. Parameterized role
export const requireRole = (role: string): CanActivateFn =>
() => inject(AuthService).hasRole(role);
5. CanActivateChild
export const adminChildGuard: CanActivateChildFn = () =>
inject(AuthService).hasRole('admin');
6. Unsaved changes
export const unsavedGuard: CanDeactivateFn<HasUnsavedChanges> = (c) =>
!c.hasUnsavedChanges();
7. Feature flag guard
export const flagGuard: CanMatchFn = (route) =>
inject(FeatureFlagService).isEnabled(route.data?.['flag']);
8. A/B test guard
export const abTest: CanMatchFn = () =>
inject(AbTestService).getGroup() === 'B';
9. Async session check
export const sessionGuard: CanActivateFn = async () => {
const auth = inject(AuthService);
return await auth.validateSession();
};
10. Observable guard
export const userGuard: CanActivateFn = () =>
inject(AuthService).user$.pipe(map(u => u !== null));
11. All-of composition
canActivate: [authGuard, roleGuard, flagGuard]
12. Any-of composition
const anyGuard: CanActivateFn = (r, s) =>
[guardA, guardB].some(g => g(r, s) === true);
13. Redirect with return URL
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
14. Combined parent guards
{
path: 'admin',
canActivate: [authGuard],
canActivateChild: [adminGuard],
children: [...]
}
15. Fall-through CanMatch
[
{ path: 'x', canMatch: [newGuard], component: NewC },
{ path: 'x', component: OldC }
]
16. Guard with route params
export const ownerGuard: CanActivateFn = (route) => {
const id = Number(route.paramMap.get('id'));
return inject(PostService).isOwner(id);
};
17. Guard with query params
export const tokenGuard: CanActivateFn = (route, state) => {
const token = new URL(state.url, 'http://x').searchParams.get('token');
return token === 'valid';
};
18. Confirmation dialog
export const confirmGuard: CanDeactivateFn<HasUnsavedChanges> = (c) =>
!c.hasUnsavedChanges() || confirm('Discard changes?');
19. Redirect to feature-disabled page
export const flagGuard: CanMatchFn = (route) => {
const enabled = inject(FeatureFlagService).isEnabled(route.data?.['flag']);
return enabled;
};
20. Guard that grants based on role hierarchy
export function atLeast(role: User['role']): CanActivateFn {
return () => {
const user = inject(AuthService).user();
if (!user) return false;
const rank = { guest: 0, user: 1, admin: 2 };
return rank[user.role] >= rank[role];
};
}
Visual: Guard Flow
┌──────────────────────────────────────────────┐
│ User navigates to /admin │
│ │ │
│ ▼ │
│ Router matches route │
│ │ │
│ ▼ │
│ CanMatch guards run │
│ │ │
│ ▼ │
│ CanActivate guards run │
│ │ │
│ ├── true → continue │
│ │ │
│ ├── false → cancel │
│ │ │
│ └── UrlTree → redirect │
│ │ │
│ ▼ │
│ CanActivateChild guards run (if children) │
│ │ │
│ ▼ │
│ Resolvers run │
│ │ │
│ ▼ │
│ Component activates │
│ │
└──────────────────────────────────────────────┘
Visual: Guard Order
┌──────────────────────────────────────────────┐
│ Navigation starts │
│ │ │
│ ▼ │
│ CanMatch │
│ │ │
│ ▼ │
│ CanActivate │
│ │ │
│ ▼ │
│ CanActivateChild │
│ │ │
│ ▼ │
│ Resolve │
│ │ │
│ ▼ │
│ Component activates │
│ │ │
│ (later, on leave) │
│ ▼ │
│ CanDeactivate │
│ │
└──────────────────────────────────────────────┘
Visual: Return Values
┌──────────────────────────────────────────────┐
│ Return true │
│ │ │
│ ▼ │
│ Navigation proceeds │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Return false │
│ │ │
│ ▼ │
│ Navigation cancelled │
│ URL stays the same │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Return UrlTree │
│ │ │
│ ▼ │
│ Navigation cancelled │
│ New navigation starts │
│ → router.createUrlTree(['/login']) │
│ │
└──────────────────────────────────────────────┘
Visual: CanMatch Fall-Through
┌──────────────────────────────────────────────┐
│ URL: /dashboard │
│ │ │
│ ▼ │
│ Route 1: canMatch: [flagGuard] │
│ │ │
│ ├── true → use this route │
│ │ │
│ └── false → try next route │
│ │
│ ▼ │
│ Route 2: no guard │
│ │ │
│ └── matches → use this route │
│ │
└──────────────────────────────────────────────┘
Visual: CanDeactivate
┌──────────────────────────────────────────────┐
│ User on /edit with unsaved changes │
│ │ │
│ ▼ │
│ Navigates away │
│ │ │
│ ▼ │
│ CanDeactivate runs │
│ │ │
│ ├── hasUnsavedChanges() true → prompt │
│ │ │
│ │ ├── OK → navigate │
│ │ └── Cancel → stay │
│ │ │
│ └── false → navigate │
│ │
└──────────────────────────────────────────────┘
Visual: Parameterized Guards
┌──────────────────────────────────────────────┐
│ function requireRole(role: string) { │
│ return (): CanActivateFn => { │
│ return () => { │
│ const auth = inject(AuthService); │
│ return auth.hasRole(role); │
│ }; │
│ }; │
│ } │
│ │
│ export const adminGuard = requireRole('admin');│
│ export const userGuard = requireRole('user');│
│ │
│ Each closes over a specific role │
│ │
└──────────────────────────────────────────────┘
Visual: Composition
┌──────────────────────────────────────────────┐
│ canActivate: [authGuard, roleGuard, flagGuard]│
│ │
│ Router runs in order: │
│ 1. authGuard │
│ 2. roleGuard (if 1 passed) │
│ 3. flagGuard (if 2 passed) │
│ │
│ First false → stop, cancel navigation │
│ All true → proceed │
│ │
└──────────────────────────────────────────────┘
Visual: Redirect with returnUrl
┌──────────────────────────────────────────────┐
│ Not logged in → navigating to /admin │
│ │ │
│ ▼ │
│ authGuard returns UrlTree: │
│ /login?returnUrl=%2Fadmin │
│ │ │
│ ▼ │
│ User logs in │
│ │ │
│ ▼ │
│ Redirect to returnUrl (/admin) │
│ │
└──────────────────────────────────────────────┘
Visual: Guard Decision Flow
┌──────────────────────────────────────────────┐
│ Protecting a route? │
│ └── CanActivate │
│ │
│ Protecting all children? │
│ └── CanActivateChild │
│ │
│ Warning before leaving? │
│ └── CanDeactivate │
│ │
│ Choosing between routes? │
│ └── CanMatch │
│ │
│ Lazy loading? │
│ └── Use CanMatch, not CanLoad │
│ │
│ Multiple conditions? │
│ └── Compose with arrays │
│ │
│ Need a specific role? │
│ └── Parameterized guard factory │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
| Route guard | Function deciding if navigation proceeds |
CanActivate | Before activating a route |
CanActivateChild | Before activating children |
CanDeactivate | Before leaving a route |
CanMatch | During matching — choose route |
CanLoad | Deprecated — use CanMatch |
| Return values | true, false, UrlTree, promise, observable |
| Functional guards | Plain functions with inject() |
| Composition | Arrays of guards, all must pass |
Key takeaways:
- Route guards decide whether navigation proceeds
CanActivate— before a route activatesCanActivateChild— before each child of a parent activatesCanDeactivate— before leaving a route, with the componentCanMatch— during matching, chooses among same-path routesCanLoadis deprecated — useCanMatchinstead- Return
trueto allow,falseto deny,UrlTreeto redirect - Async guards return promises or observables — the router waits
- Functional guards —
CanActivateFnand friends — useinject()for dependencies - Parameterized guards — factory functions returning guards for specific roles/flags
- Compose guards in arrays — all must return truthy
CanDeactivatereceives the component — use an interface for the contractCanMatchfalls through to the next route if it returnsfalse- Guards run before the component loads — denied routes don’t download code
- Test with
TestBed.runInInjectionContextfor the injection context
Remember: Guards are the app’s access control. CanActivate protects a route, CanActivateChild protects a group, CanDeactivate prompts before leaving, CanMatch chooses routes. Functional guards are the modern form — a function with inject() that returns a boolean or a UrlTree. Compose them in arrays for complex rules. Use parameterized factories for role-based checks. Remember the order: match, activate, activate child, resolve, activate. And the whole point — deny before the code loads, so unauthorized users never see what they shouldn’t.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!