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 Router loaders/guards are also function-based; Angular's pipeline ordering mirrors the concept.CanActivateFn/ResolveFn — the current topic.Vue Router navigation guards are functions with DI via the app context.SvelteKit load functions + guards are the function-first analogue.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().
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService) {} // ❌ OLD: class + DI constructor
canActivate() { return this.auth.isLoggedIn(); }
}export const authGuard: CanActivateFn = () => inject(AuthService).isLoggedIn(); // ✅ NEWInterview 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.
Context
This probes the guard pipeline semantics — return-type handling and ordering — which determine navigation outcomes in real apps.
The mechanical answer
A UrlTree return causes the router to cancel the current navigation and start a new one to that tree (a redirect), so guards can redirect (e.g. to /login) without injecting Router manually. A false return cancels navigation (NavigationCancel). Multiple guards in one array run sequentially in order, top-down through the route levels (parent guards before child guards for canActivate); if any returns false or a UrlTree, the remaining guards are skipped and the navigation is cancelled or redirected. Promise/Observable returns are awaited/subscribed. Resolvers run after guards pass and populate the snapshot data. This deterministic pipeline is why functional composition (auth then role then data) is safe and testable.
Trap
The naive answer is 'guards run in parallel'. They run sequentially per the router's pipeline, and ordering matters for side effects. Another trap: forgetting that a UrlTree is a redirect, not just 'deny' — returning a UrlTree to the current URL can cause a redirect loop if not guarded. Candidates who describe the ordered pipeline and the false-vs-UrlTree distinction show real depth.