| |

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 @Injectable and 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 uses inject() for dependencies.


The five guard types

Angular has five types, each for a different moment.

GuardRuns whenPurpose
CanActivateBefore route activatesAllow/deny navigation to a route
CanActivateChildBefore child route activatesProtect all children of a parent
CanDeactivateBefore leaving a routePrompt for unsaved changes
CanMatchBefore matching a routeChoose among routes with the same path
CanLoadBefore loading a lazy moduleDeprecated — use CanMatch

The three you’ll use most:

  • CanActivate — protect a route from unauthorized users
  • CanDeactivate — prompt before leaving with unsaved changes
  • CanMatch — route feature flags and A/B tests

The one you’ll rarely use:

  • CanActivateChild — same as CanActivate on the parent for all children

The deprecated one:

  • CanLoad — replaced by CanMatch

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 CanLoad is deprecated: It only worked for lazy modules. CanMatch works for any route — lazy or not — and gives more control. The Angular team recommends CanMatch for 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 activated
  • state: RouterStateSnapshot — the full router state

What it returns:

  • true — allow navigation
  • false — cancel navigation
  • UrlTree — 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: CanActivate fires when the parent activates — but if you navigate from /admin/users to /admin/settings, the parent stays active. CanActivate doesn’t re-fire. CanActivateChild does — 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 (for CanDeactivate). 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. CanMatch decides 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 /login with returnUrl
  • adminGuard — as CanActivateChild, protects every child of /admin
  • featureFlagGuard — as CanMatch, chooses the new dashboard when the flag is on
  • unsavedChangesGuard — as CanDeactivate, prompts before leaving /edit
  • CompositionauthGuard on /admin itself, adminGuard on 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

GuardRunsPurpose
CanActivateBefore activatingAllow/deny route
CanActivateChildBefore child activatesProtect children
CanDeactivateBefore leavingPrompt for changes
CanMatchDuring matchingChoose route
CanLoadBefore lazy loadDeprecated

Functional Types

TypeSignature
CanActivateFn(route, state) => boolean | UrlTree | Promise | Observable
CanActivateChildFnSame as CanActivateFn
CanDeactivateFn<T>(component, currentRoute, nextState) => ...
CanMatchFn(route, segments) => boolean | UrlTree

Return Values

ReturnEffect
trueAllow
falseDeny
UrlTreeRedirect
Promise<...>Async — router waits
Observable<...>Async — first emission

Guard Arguments

GuardReceives
CanActivateroute (snapshot), state
CanActivateChildchildRoute, state
CanDeactivatecomponent, currentRoute, nextState
CanMatchroute (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

FormMeaning
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

OrderGuard
1CanMatch
2CanActivate
3CanActivateChild
4Resolvers
5Activate component
6CanDeactivate (on leave)

Common Errors

ErrorCause
Cannot find name 'inject'Missing import
inject outside contextGuard called in wrong place
Guard returned undefinedMissing return
Function not assignableWrong 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

GuardFrequency
CanActivateOn activation
CanActivateChildOn each child activation
CanDeactivateOn leaving
CanMatchOn matching
CanLoadOn 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

PitfallProblemSolution
No returnNavigation blockedAlways return a value
Async without awaitReturns undefinedReturn the promise
inject() outside bodyRuntime errorCall inside the function
CanMatch returns PromiseNot supportedReturn boolean or UrlTree
Forgot canActivateChildChildren unguardedAdd on parent
CanDeactivate on wrong propertyDoesn’t fireUse canDeactivate
Missing UrlTree importCompile errorImport from @angular/router
Guards on the wrong levelWrong behaviorMatch to the guard type
Testing without contextinject() failsTestBed.runInInjectionContext
Async guard race conditionsFlickering navigationReturn 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

ConceptMeaning
Route guardFunction deciding if navigation proceeds
CanActivateBefore activating a route
CanActivateChildBefore activating children
CanDeactivateBefore leaving a route
CanMatchDuring matching — choose route
CanLoadDeprecated — use CanMatch
Return valuestrue, false, UrlTree, promise, observable
Functional guardsPlain functions with inject()
CompositionArrays of guards, all must pass

Key takeaways:

  • Route guards decide whether navigation proceeds
  • CanActivate — before a route activates
  • CanActivateChild — before each child of a parent activates
  • CanDeactivate — before leaving a route, with the component
  • CanMatch — during matching, chooses among same-path routes
  • CanLoad is deprecated — use CanMatch instead
  • Return true to allow, false to deny, UrlTree to redirect
  • Async guards return promises or observables — the router waits
  • Functional guardsCanActivateFn and friends — use inject() for dependencies
  • Parameterized guards — factory functions returning guards for specific roles/flags
  • Compose guards in arrays — all must return truthy
  • CanDeactivate receives the component — use an interface for the contract
  • CanMatch falls through to the next route if it returns false
  • Guards run before the component loads — denied routes don’t download code
  • Test with TestBed.runInInjectionContext for 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!