Skip to content

Angular Route Guards Explained — Protect Your Routes

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Angular Route Guards Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Angular route guards control whether a user can navigate to, away from, or load a route, enabling authentication checks, role-based access, and data preloading.

What You'll Learn

  • The different guard types: CanActivate, CanActivateChild, CanDeactivate, CanLoad
  • How to create functional route guards
  • How to restrict routes based on authentication
  • How to restrict routes based on user roles
  • How to prevent unsaved changes with CanDeactivate

Why It Matters

Guards protect your application from unauthorized access. Without guards, any user could navigate to admin panels, edit other users' data, or leave forms with unsaved changes. They are the first line of defense in your app's security.

Real-World Use

Durga Antivirus Pro uses guards at multiple levels: an auth guard checks the session token before allowing access to the dashboard, an admin role guard restricts system settings, and a CanDeactivate guard warns users before leaving a scan configuration with unsaved changes.

flowchart TD
    A[User Clicks Link] --> B[Router]
    B --> C{CanActivate?}
    C -->|Yes| D[Activate Route]
    C -->|No| E[Redirect to Login]
    D --> F{CanDeactivate?}
    F -->|Yes| G[Leave Route]
    F -->|No| H[Stay on Route]
    style A fill:#f97316,color:#fff

CanActivate Guard

Controls access to a route:

import { Injectable, inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";

export const authGuard: CanActivateFn = (route, state) => {
  const router = inject(Router);
  const token = localStorage.getItem("auth_token");

  if (token) {
    return true;
  }

  router.navigate(["/login"], {
    queryParams: { returnUrl: state.url }
  });
  return false;
};

Route configuration:

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

export const routes: Routes = [
  { path: "login", component: LoginComponent },
  {
    path: "dashboard",
    component: DashboardComponent,
    canActivate: [authGuard]
  },
  {
    path: "admin",
    canActivate: [authGuard],
    children: [
      { path: "users", component: UserManagementComponent },
      { path: "settings", component: AdminSettingsComponent },
    ]
  },
  { path: "**", redirectTo: "/login" }
];

Expected output: Navigating to /dashboard when not logged in redirects to /login?returnUrl=/dashboard. After login, the user is redirected back.

CanActivate returns true (allow navigation), false (deny), a UrlTree (redirect), or an Observable/Promise that resolves to any of these.

CanActivateChild Guard

Protects all child routes of a parent:

import { CanActivateChildFn, Router } from "@angular/router";
import { inject } from "@angular/core";

export const adminGuard: CanActivateChildFn = (route, state) => {
  const router = inject(Router);
  const role = localStorage.getItem("user_role");

  if (role === "admin") {
    return true;
  }

  router.navigate(["/dashboard"]);
  return false;
};

Route configuration:

{
  path: "admin",
  canActivate: [authGuard],
  canActivateChild: [adminGuard],
  children: [
    { path: "users", component: UserManagementComponent },
    { path: "logs", component: AuditLogComponent },
    { path: "settings", component: SystemSettingsComponent },
  ]
}

Expected output: Only users with the "admin" role can access any child route under /admin. Non-admin users are redirected to /dashboard.

CanActivateChild runs before each child route is activated. It is more efficient than adding canActivate to every child route individually.

CanDeactivate Guard

Prevent leaving a route with unsaved changes:

import { CanDeactivateFn } from "@angular/router";
import { inject } from "@angular/core";

export interface CanComponentDeactivate {
  canDeactivate: () => boolean | Promise<boolean>;
}

export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (
  component: CanComponentDeactivate
) => {
  if (component.canDeactivate) {
    return component.canDeactivate();
  }
  return true;
};

Component implementing the guard:

import { Component } from "@angular/core";
import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
import { CanComponentDeactivate } from "./guards/unsaved-changes.guard";

@Component({
  selector: "app-edit-profile",
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="profileForm">
      <input formControlName="name" placeholder="Name" />
      <input formControlName="email" placeholder="Email" />
      <button type="submit">Save</button>
    </form>
  `
})
export class EditProfileComponent implements CanComponentDeactivate {
  profileForm = new FormBuilder().group({
    name: ["", Validators.required],
    email: ["", Validators.required],
  });
  saved = false;

  canDeactivate(): boolean {
    if (this.profileForm.dirty && !this.saved) {
      return confirm("You have unsaved changes. Discard them?");
    }
    return true;
  }
}

Route configuration:

{
  path: "profile/edit",
  component: EditProfileComponent,
  canDeactivate: [unsavedChangesGuard]
}

Expected output: Attempting to navigate away from the edit profile form with unsaved changes triggers a browser confirm dialog.

The guard calls the component's canDeactivate method. The component decides whether navigation should proceed based on its form state. Returning a Promise enables async checks.

CanMatch Guard (Angular 14+)

Conditionally match routes based on runtime conditions:

import { CanMatchFn, Router } from "@angular/router";
import { inject } from "@angular/core";

export const featureFlagGuard: CanMatchFn = (route, segments) => {
  const flags = {
    "new-dashboard": true,
    "beta-analytics": false,
    "experimental-charts": false,
  };

  const featureName = route.path || "";
  return flags[featureName] !== false;
};

Route configuration:

{
  path: "new-dashboard",
  loadComponent: () => import("./new-dashboard/new-dashboard.component").then(m => m.NewDashboardComponent),
  canMatch: [featureFlagGuard]
},
{
  path: "dashboard",
  component: DashboardComponent
}

Expected output: Users with the feature flag enabled see the new dashboard; others see the regular dashboard. The route that matches first wins.

CanMatch (replaces CanLoad in Angular 14+) decides whether a route configuration should be considered when matching URLs. This is ideal for A/B testing, feature flags, and gradual rollouts.

Guard with Dependency Injection

Guards can inject services for complex logic:

import { Injectable, inject } from "@angular/core";
import { CanActivateFn } from "@angular/router";
import { HttpClient } from "@angular/common/http";
import { map, catchError, of } from "rxjs";

export const subscriptionGuard: CanActivateFn = () => {
  const http = inject(HttpClient);

  return http.get<{ active: boolean }>("/api/subscription/status").pipe(
    map(response => {
      if (!response.active) {
        return inject(Router).createUrlTree(["/pricing"]);
      }
      return true;
    }),
    catchError(() => {
      return of(inject(Router).createUrlTree(["/error"]));
    })
  );
};

Expected output: Before navigating to premium features, the guard checks the subscription status. Inactive subscribers are redirected to the pricing page.

Functional guards can inject dependencies using inject(). For async checks, return an Observable or Promise. Returning a UrlTree causes a redirect to the specified URL.

Common Mistakes

  1. Forgetting to inject Router for redirects — If you return false without redirecting, the navigation silently fails. Always provide user feedback.

  2. Blocking all navigation with CanDeactivate — A broken CanDeactivate guard can trap users on a page. Always include a way to bypass the guard (like the saved flag in the example).

  3. Not handling guard errors — If a guard throws, navigation fails. Use catchError operator to handle errors gracefully.

  4. Overusing guards for data loading — Guards should check conditions, not fetch data. Use Resolvers for data preloading.

  5. Caching auth state too long — If the auth token expires, the guard may incorrectly allow access based on stale cached state.

Practice Questions

  1. What is the purpose of CanActivate? To decide whether a route can be activated based on conditions like authentication status.

  2. How do guards redirect to a different route? Return a UrlTree created with router.createUrlTree(["/path"]). The router will redirect automatically.

  3. What is the difference between CanActivate and CanActivateChild? CanActivate protects a single route. CanActivateChild protects all child routes of a parent.

  4. What does CanDeactivate guard? Whether a user can navigate away from the current route. Used to prevent losing unsaved data.

  5. What replaced CanLoad in Angular 14+? CanMatch. It provides the same feature set with better integration with the router's matching logic.

Challenge

Build a MultiFactorGuard that checks if the user has completed MFA verification. If not, redirect to a MFA verification page with the return URL in query params. After successful MFA, redirect back to the original destination. Store MFA status in a service that persists across page refreshes.

FAQ

Can guards be async?

Yes, return an Observable (from HTTP calls) or Promise. The router waits for the guard to resolve before navigating.

Do guards run in order?

Yes, guards in the canActivate array run sequentially. If any guard returns false, navigation stops.

Can I inject services into functional guards?

Yes, use the inject() function inside the guard to access Angular services.

What happens if a guard does not return anything?

The guard returns undefined, which is treated as falsy. Navigation is blocked.

Are guards called on every navigation?

Yes, guards run every time the route is activated, not just on first load.

Mini Project

Build a PermissionSystemComponent with three levels: auth guard (logged in users), admin guard (role check), and feature flag guard (canMatch). Create a route tree with public routes, user routes, admin routes, and beta feature routes. Add a login component that sets the auth state and role. The navigation menu should only show routes the current user can access.

What's Next

Continue with resolvers and Lazy Loading:

Angular Resolvers, Angular Lazy Loading, Angular HTTP Interceptors

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro