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 Type Purpose When It Runs CanActivateControls if a route can be activated Before route activation CanActivateChildControls if child routes can be activated Before child route activation CanDeactivateControls if a route can be deactivated Before leaving current route CanMatchControls if a route can match the URL During route matching phase ResolvePre-fetches data before activating route Before 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.
Functional Guards (Recommended)
Modern Angular uses functional guards with the inject() function for dependency injection:
CanActivate Guard
Controls whether a route can be activated:
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 }
});
};
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:
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' ]);
};
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:
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 ;
};
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:
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 ;
};
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...
}
}
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:
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 ;
};
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:
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' ]);
})
);
};
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:
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:
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 );
};
import { userResolver } from './resolvers/user.resolver' ;
export const routes : Routes = [
{
path: 'user/:id' ,
component: UserDetailComponent ,
resolve: {
user: userResolver // Data available before component loads
}
}
];
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' ];
});
}
}
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:
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:
canDeactivate - Current route’s deactivation guards
canMatch - Route matching guards
canLoad - Lazy loading guards (deprecated, use canMatch)
canActivateChild - Parent route’s child activation guards
canActivate - Route activation guards
resolve - Data resolvers
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:
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