Under The Hood
frontend angular Angular 15+ (functional guards/resolvers)

Functional CanActivateFn vs Legacy Class Guards

Last updated
Prerequisites:
Angular Router basics (routes, guards)
Understanding of inject() and DI
  • angular
  • router
  • guards
  • resolvers
  • CanActivateFn
  • functional

Read at your depth

The practical view

Angular guards/resolvers can be plain functions now: export const authGuard: CanActivateFn = () => inject(AuthService).isLoggedIn();. Register them directly in the route: { path: 'admin', canActivate: [authGuard] }. Functional guards can return boolean, UrlTree, or Observable/Promise of those. Resolvers: const userResolver: ResolveFn<User> = () => inject(UserService).load();. They use inject() for DI, so no @Injectable class or constructor boilerplate. They compose better: canActivate: [authGuard, roleGuard] runs them in order.

The same idea in other frameworks

react equivalent
React Router loaders/guards are also function-based; Angular's pipeline ordering mirrors the concept.

Legacy vs modern

Class guard with constructor DI vs functional guard with inject()

Class guards need @Injectable + constructor + provider wiring; functional guards are single-purpose functions using inject().

before → after
Class-based guard
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private auth: AuthService) {} // ❌ OLD: class + DI constructor
  canActivate() { return this.auth.isLoggedIn(); }
}
Functional guard
export const authGuard: CanActivateFn = () => inject(AuthService).isLoggedIn(); // ✅ NEW

Interview gotchas

Context

This tests the injection-context model applied to the router — interviewers want to know what makes guard execution special and where inject() is legal.

The mechanical answer

The router executes guards inside an injection context: when a navigation runs, the router sets up the environment injector (and route-level injectors) as the active injector before invoking guard functions. Therefore inject() inside the guard resolves against that context — services like AuthService, Router, or ActivatedRouteSnapshot data are available. Regular functions called from an event handler have no such ambient injector (the context is null), so inject() throws NG0203. This is why guards/resolvers can be plain functions: they are contractually executed within the DI context by the router. The same applies to providers' factory functions and interceptor functions.

Trap

The naive answer is 'inject works in any function that imports it'. It works only in an active injection context; the router provides that for guards. Another trap: assuming you can inject route params directly — you must read them from the ActivatedRouteSnapshot argument or inject ActivatedRoute, not via a magic global. Candidates who explain the router-provided injection context show real understanding.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.