Skip to main content
Route guards are functions or classes that the Angular router executes to determine whether a navigation should be allowed or denied. Guards can protect routes from unauthorized access, prevent unsaved changes from being lost, and fetch data before activating routes.

Types of Guards

Angular provides several types of guards for different scenarios:
Guard TypePurposeWhen It Runs
CanActivateControls if a route can be activatedBefore route activation
CanActivateChildControls if child routes can be activatedBefore child route activation
CanDeactivateControls if a route can be deactivatedBefore leaving current route
CanMatchControls if a route can match the URLDuring route matching phase
ResolvePre-fetches data before activating routeBefore route activation
Guard interfaces are defined in packages/router/src/models.ts starting at line 857. The router executes guards using the logic in packages/router/src/operators/check_guards.ts.
Modern Angular uses functional guards with the inject() function for dependency injection:

CanActivate Guard

Controls whether a route can be activated:
auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  if (authService.isLoggedIn()) {
    return true;
  }
  
  // Redirect to login page
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};
routes.ts
import { authGuard } from './guards/auth.guard';

export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard] // Protect this route
  },
  {
    path: 'profile',
    component: ProfileComponent,
    canActivate: [authGuard] // Multiple guards can be applied
  }
];

Multiple Guards

You can apply multiple guards to a single route:
admin.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const adminGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  if (authService.hasRole('admin')) {
    return true;
  }
  
  // Redirect to access denied page
  return router.createUrlTree(['/access-denied']);
};
routes.ts
export const routes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [authGuard, adminGuard] // Both must pass
  }
];
Guards execute in order. If any guard returns false or redirects, subsequent guards won’t execute.

CanActivateChild Guard

Protects child routes:
parent-auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateChildFn } from '@angular/router';
import { AuthService } from './auth.service';

export const parentAuthGuard: CanActivateChildFn = (childRoute, state) => {
  const authService = inject(AuthService);
  
  // Check permissions for child routes
  const requiredPermission = childRoute.data?.['permission'];
  
  if (requiredPermission) {
    return authService.hasPermission(requiredPermission);
  }
  
  return true;
};
routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivateChild: [parentAuthGuard], // Applies to all children
    children: [
      { 
        path: 'reports', 
        component: ReportsComponent,
        data: { permission: 'view-reports' }
      },
      { 
        path: 'settings', 
        component: SettingsComponent,
        data: { permission: 'edit-settings' }
      }
    ]
  }
];

CanDeactivate Guard

Prevents users from leaving a route with unsaved changes:
unsaved-changes.guard.ts
import { CanDeactivateFn } from '@angular/router';

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

export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = 
  (component, currentRoute, currentState, nextState) => {
    // If component has unsaved changes, ask for confirmation
    if (component.canDeactivate) {
      return component.canDeactivate();
    }
    return true;
  };
form.component.ts
import { Component } from '@angular/core';
import { CanComponentDeactivate } from './guards/unsaved-changes.guard';

@Component({
  selector: 'app-form',
  template: `
    <form (ngSubmit)="save()">
      <input [(ngModel)]="data" name="data">
      <button type="submit">Save</button>
    </form>
  `
})
export class FormComponent implements CanComponentDeactivate {
  data = '';
  private savedData = '';

  canDeactivate(): boolean {
    // Check if there are unsaved changes
    if (this.data !== this.savedData) {
      return window.confirm('You have unsaved changes. Do you want to leave?');
    }
    return true;
  }

  save() {
    this.savedData = this.data;
    // Save logic...
  }
}
routes.ts
import { unsavedChangesGuard } from './guards/unsaved-changes.guard';

export const routes: Routes = [
  {
    path: 'edit/:id',
    component: FormComponent,
    canDeactivate: [unsavedChangesGuard]
  }
];

CanMatch Guard

Determines if a route configuration can be used:
feature-toggle.guard.ts
import { inject } from '@angular/core';
import { CanMatchFn } from '@angular/router';
import { FeatureService } from './feature.service';

export const featureToggleGuard: CanMatchFn = (route, segments) => {
  const featureService = inject(FeatureService);
  const featureName = route.data?.['feature'];
  
  if (featureName) {
    return featureService.isEnabled(featureName);
  }
  
  return true;
};
routes.ts
export const routes: Routes = [
  {
    path: 'beta-feature',
    component: BetaFeatureComponent,
    canMatch: [featureToggleGuard],
    data: { feature: 'beta-feature' }
  },
  {
    // Fallback route when feature is disabled
    path: 'beta-feature',
    component: ComingSoonComponent
  }
];
CanMatch guards are useful for A/B testing, feature flags, and conditional route loading. Unlike CanActivate, they prevent the route from being recognized at all.

Async Guards

Guards can return Observables or Promises for async operations:
permissions.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { PermissionsService } from './permissions.service';
import { map } from 'rxjs/operators';

export const permissionsGuard: CanActivateFn = (route, state) => {
  const permissionsService = inject(PermissionsService);
  const router = inject(Router);
  const requiredPermission = route.data?.['permission'];
  
  // Return Observable<boolean>
  return permissionsService.hasPermission(requiredPermission).pipe(
    map(hasPermission => {
      if (hasPermission) {
        return true;
      }
      return router.createUrlTree(['/access-denied']);
    })
  );
};
async-auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const asyncAuthGuard: CanActivateFn = async (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  try {
    // Return Promise<boolean>
    const isAuthenticated = await authService.checkAuthentication();
    
    if (isAuthenticated) {
      return true;
    }
    
    return router.createUrlTree(['/login']);
  } catch (error) {
    console.error('Authentication check failed:', error);
    return router.createUrlTree(['/error']);
  }
};

RedirectCommand

Use RedirectCommand for more control over redirects:
redirect.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router, RedirectCommand } from '@angular/router';
import { AuthService } from './auth.service';

export const redirectGuard: CanActivateFn = (route, state) => {
  const router = inject(Router);
  const authService = inject(AuthService);
  
  if (!authService.isLoggedIn()) {
    const loginUrl = router.parseUrl('/login');
    
    // RedirectCommand provides more control
    return new RedirectCommand(loginUrl, {
      skipLocationChange: true, // Don't update browser URL
      replaceUrl: true, // Replace current history entry
      state: { returnUrl: state.url } // Pass state to target route
    });
  }
  
  return true;
};
RedirectCommand is defined in packages/router/src/models.ts:118 and provides fine-grained control over navigation behavior during redirects.

Data Resolvers

Resolvers fetch data before a route activates:
user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn, Router, RedirectCommand } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';

export const userResolver: ResolveFn<User> = (route, state) => {
  const userService = inject(UserService);
  const router = inject(Router);
  const userId = route.paramMap.get('id');
  
  if (!userId) {
    // Redirect if no ID provided
    return new RedirectCommand(router.parseUrl('/users'));
  }
  
  // Fetch user data
  return userService.getUser(userId);
};
routes.ts
import { userResolver } from './resolvers/user.resolver';

export const routes: Routes = [
  {
    path: 'user/:id',
    component: UserDetailComponent,
    resolve: {
      user: userResolver // Data available before component loads
    }
  }
];
user-detail.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { User } from './user.model';

@Component({
  selector: 'app-user-detail',
  template: `
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  `
})
export class UserDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  user!: User;

  ngOnInit() {
    // Data is already resolved
    this.route.data.subscribe(data => {
      this.user = data['user'];
    });
  }
}

With Component Input Binding

user-detail.component.ts
import { Component, Input } from '@angular/core';
import { User } from './user.model';

@Component({
  selector: 'app-user-detail',
  template: `
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  `
})
export class UserDetailComponent {
  @Input() user!: User; // Automatically injected from resolver
}

Class-Based Guards (Legacy)

While functional guards are recommended, class-based guards are still supported:
auth.guard.ts
import { Injectable } from '@angular/core';
import { 
  CanActivate, 
  ActivatedRouteSnapshot, 
  RouterStateSnapshot,
  Router 
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    if (this.authService.isLoggedIn()) {
      return true;
    }
    
    this.router.navigate(['/login'], {
      queryParams: { returnUrl: state.url }
    });
    return false;
  }
}
Class-based guards are deprecated. Use functional guards with inject() for new code. They’re more concise and tree-shakeable.

Guard Execution Order

When multiple guards are present, they execute in this order:
  1. canDeactivate - Current route’s deactivation guards
  2. canMatch - Route matching guards
  3. canLoad - Lazy loading guards (deprecated, use canMatch)
  4. canActivateChild - Parent route’s child activation guards
  5. canActivate - Route activation guards
  6. resolve - Data resolvers
routes.ts
export const routes: Routes = [
  {
    path: 'protected',
    canMatch: [featureToggleGuard], // 1st
    canActivate: [authGuard, adminGuard], // 2nd & 3rd
    canActivateChild: [parentAuthGuard], // Applies to children
    component: ProtectedComponent,
    resolve: {
      data: dataResolver // Last, only if guards pass
    },
    children: [
      {
        path: 'child',
        component: ChildComponent,
        canDeactivate: [unsavedChangesGuard] // When leaving
      }
    ]
  }
];

Real-World Example

Comprehensive guard setup for an enterprise application:
app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
import { roleGuard } from './guards/role.guard';
import { unsavedChangesGuard } from './guards/unsaved-changes.guard';
import { featureToggleGuard } from './guards/feature-toggle.guard';
import { userResolver } from './resolvers/user.resolver';

export const routes: Routes = [
  // Public routes
  { path: 'login', component: LoginComponent },
  { path: 'home', component: HomeComponent },
  
  // Protected routes
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard],
    children: [
      {
        path: 'profile',
        component: ProfileComponent,
        resolve: { user: userResolver }
      },
      {
        path: 'settings',
        component: SettingsComponent,
        canDeactivate: [unsavedChangesGuard]
      }
    ]
  },
  
  // Admin routes
  {
    path: 'admin',
    canActivate: [authGuard, roleGuard],
    canActivateChild: [roleGuard],
    data: { roles: ['admin', 'superadmin'] },
    children: [
      { path: 'users', component: AdminUsersComponent },
      { path: 'reports', component: AdminReportsComponent }
    ]
  },
  
  // Feature flagged route
  {
    path: 'beta',
    canMatch: [featureToggleGuard],
    data: { feature: 'beta-feature' },
    component: BetaFeatureComponent
  }
];

Best Practices

Use Functional Guards

Prefer functional guards with inject() over class-based guards for better tree-shaking and simplicity.

Return UrlTree for Redirects

Return a UrlTree from guards instead of calling router.navigate() to let the router handle the redirect.

Handle Errors Gracefully

Always handle errors in async guards and provide fallback behavior.

Keep Guards Focused

Each guard should have a single responsibility. Combine multiple guards instead of creating complex logic in one.

Next Steps

Lazy Loading

Learn how to implement lazy loading to optimize your application

Router Overview

Review the fundamentals of Angular routing