Skip to content
Angular ng routing 4 min read

Functional Route Guards

Route guards decide whether navigation to or away from a route is allowed. Since Angular 14, guards are plain functions instead of class-based services, and with inject() they can pull in any dependency they need — services, the router, signals — without constructors or @Injectable. Functional guards are the modern standard: they are smaller, composable, tree-shakable, and trivially testable. This page covers the three guards you reach for most: CanActivateFn, CanMatchFn, and CanDeactivateFn.

How functional guards work

A functional guard is a function with a typed signature. It runs inside an injection context, so you can call inject() at the top to grab dependencies. It returns a boolean, a UrlTree (to redirect), or an Observable/Promise resolving to one of those. Returning true allows navigation; false blocks it; a UrlTree cancels the current navigation and starts a new one.

You attach guards directly in the route config:

import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard.component'),
    canActivate: [authGuard],
  },
];

CanActivateFn — allow or block entry

CanActivateFn runs before a route is activated. The classic use case is authentication: redirect anonymous users to the login page. Because the guard returns a UrlTree, the redirect is handled by the router itself — no manual navigate() call needed.

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isLoggedIn()) {
    return true;
  }

  // Redirect to login, preserving the attempted URL
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url },
  });
};

The guard receives the ActivatedRouteSnapshot and RouterStateSnapshot, giving you access to route params, data, and the target URL. You can use route.data to pass role requirements and build a single reusable guard:

export const roleGuard: CanActivateFn = (route) => {
  const auth = inject(AuthService);
  const required = route.data['role'] as string;
  return auth.hasRole(required) || inject(Router).createUrlTree(['/forbidden']);
};
{ path: 'admin', component: AdminComponent, canActivate: [roleGuard], data: { role: 'admin' } }

CanMatchFn — control whether a route matches at all

CanMatchFn runs before the router commits to a route, during path matching. If it returns false, the router skips this route and continues evaluating later routes with the same path. This is more powerful than canActivate for two reasons: it lets you show different components for the same URL depending on conditions, and it prevents the route’s lazy chunk from being downloaded when access is denied.

import { inject } from '@angular/core';
import { CanMatchFn, Router } from '@angular/router';
import { FeatureFlags } from '../services/feature-flags.service';

export const betaGuard: CanMatchFn = () => {
  const flags = inject(FeatureFlags);
  return flags.isEnabled('beta-ui') ? true : inject(Router).createUrlTree(['/']);
};
export const routes: Routes = [
  {
    path: 'reports',
    canMatch: [betaGuard],
    loadComponent: () => import('./reports-v2.component'),
  },
  {
    path: 'reports',
    loadComponent: () => import('./reports-v1.component'),
  },
];

Use canMatch instead of canActivate when you want to gate lazy-loaded code. With canActivate the chunk still downloads before the guard rejects; with canMatch the router never loads it.

CanDeactivateFn — confirm before leaving

CanDeactivateFn runs when the user navigates away from a route. The most common use is warning about unsaved form changes. The guard is generic over the component type, so it receives the actual component instance and can read its state.

import { CanDeactivateFn } from '@angular/router';

export interface FormComponent {
  hasUnsavedChanges(): boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<FormComponent> = (component) => {
  if (component.hasUnsavedChanges()) {
    return confirm('You have unsaved changes. Leave anyway?');
  }
  return true;
};

The component simply exposes the method the guard expects:

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-edit',
  template: `<input [value]="name()" (input)="dirty.set(true)" />`,
})
export class EditComponent {
  name = signal('');
  dirty = signal(false);
  hasUnsavedChanges() {
    return this.dirty();
  }
}

Guard return types and timing

GuardWhen it runsReceivesTypical use
CanActivateFnBefore activating a matched routesnapshot, router stateAuth, role checks
CanActivateChildFnBefore activating any child routesnapshot, router stateSection-wide auth
CanMatchFnDuring path matching, before loadroute, segmentsFeature flags, lazy gating
CanDeactivateFn<T>When leaving a routecomponent instanceUnsaved-change prompts

All of them accept the same return shape:

type GuardResult = boolean | UrlTree;
type MaybeAsync<T> = T | Observable<T> | Promise<T>;

Testing a functional guard

Because guards are functions, you test them by running inside an injection context with TestBed.runInInjectionContext:

import { TestBed } from '@angular/core/testing';

it('allows access when logged in', () => {
  TestBed.configureTestingModule({
    providers: [{ provide: AuthService, useValue: { isLoggedIn: () => true } }],
  });
  const result = TestBed.runInInjectionContext(() =>
    authGuard({} as any, { url: '/dashboard' } as any),
  );
  expect(result).toBe(true);
});

Output:

✓ allows access when logged in (12 ms)

Tests:       1 passed, 1 total

Best Practices

  • Prefer functional guards over class-based ones; they are the recommended approach and class guards are effectively legacy.
  • Return a UrlTree for redirects rather than calling router.navigate() — it keeps navigation cancellation atomic.
  • Use canMatch to gate lazy-loaded routes so unauthorized users never download the chunk.
  • Drive role/permission checks from route.data so one guard serves many routes.
  • Keep guards thin — delegate real logic to injected services and signals for easy testing and reuse.
  • Compose multiple small guards in the array (canActivate: [authGuard, roleGuard]) instead of one monolithic guard.
Last updated June 14, 2026
Was this helpful?