skip to content

In Angular's router, how does a canMatch guard returning false differ from a canActivate guard returning false?

level: middleimportance: must knowfreq 58%

answer

  1. matching phase versus activation phase
  2. skip this route, try the next
  3. same path, different component
  4. lazy children not fetched on rejection

basics

~20 s

canMatch runs while the router matches the URL: false makes it skip that route config and try the next one, possibly a ** fallback. canActivate runs after a route has matched: false cancels the whole navigation.

solid answer

~40 s

A `CanMatchFn` is called as `(route: Route, segments: UrlSegment[], currentSnapshot: PartialMatchRouteSnapshot)` during recognition. If it returns `false`, the router treats that `Route` as not matching and keeps looking, so a later route with the same path, or the `**` wildcard, can take the URL. A `canActivate` guard runs only after the router has built the target state; `false` cancels the navigation outright. That makes `canMatch` the tool for serving different components on one path by role or feature flag, and for gating a `loadChildren` section: a rejected user never triggers that navigation's download of the lazy routes. A `UrlTree` or `RedirectCommand` from either still redirects. `canMatch` replaced the deprecated `canLoad`, and since v22 its third `currentSnapshot` argument is required in the types, exposing params, data and the URL matched so far.

code

ts · 16 lines
ts
import { inject } from '@angular/core';
import { CanMatchFn, Routes } from '@angular/router';
import { AuthService } from './auth.service';

export const isAdmin: CanMatchFn = () => inject(AuthService).hasRole('admin');

export const routes: Routes = [
  {
    path: 'admin',
    canMatch: [isAdmin],
    loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
  },
  { path: 'dashboard', canMatch: [isAdmin], loadComponent: () => import('./admin/admin-dashboard').then((m) => m.AdminDashboard) },
  { path: 'dashboard', loadComponent: () => import('./user/user-dashboard').then((m) => m.UserDashboard) },
  { path: '**', loadComponent: () => import('./not-found').then((m) => m.NotFound) },
];

go deeper

for a junior

Recall that canMatch false means try the next route, while canActivate false means the navigation stops.

for a middle

Explain that canMatch runs during recognition, before loadChildren is fetched, and what currentSnapshot does and does not contain.

for a senior

Choose canMatch for role-specific screens and hidden sections, and flag that preloading ignores it and that chunks are never access control.

for a principal

Weigh hiding sections behind a 404 against explicit forbidden pages, and how route gating aligns with server authorization and bundle boundaries.

## Two different moments in a navigation Angular's router processes a navigation in stages. First it **recognizes** the URL: it walks the `Routes` configuration in order, matching path segments and loading lazy `loadChildren` configs as it goes, to build a `RouterStateSnapshot`. Then it runs **guards** on that snapshot (`canDeactivate`, then `canActivateChild` and `canActivate`), then resolvers, then activates components. `canMatch` belongs to the first stage; `canActivate` belongs to the second. That timing explains every difference between them. ## What false means for each | | `canMatch` | `canActivate` | |---|---|---| | Runs during | URL matching (recognition) | Guard checks after recognition | | Arguments | `route: Route`, `segments: UrlSegment[]`, `currentSnapshot` | `route: ActivatedRouteSnapshot`, `state: RouterStateSnapshot` | | `false` means | This route config does not match; try the next one | Cancel the whole navigation | | `UrlTree` / `RedirectCommand` | Redirect | Redirect | | Lazy `loadChildren` of a rejected route | Not fetched by this navigation | Already loaded during recognition | | Fires `GuardsCheckStart` / `GuardsCheckEnd` | No, it is part of recognition | Yes | ## Same path, different screens Because a rejected `canMatch` just moves on, two routes may share a path: 1. `{ path: 'dashboard', component: AdminDashboard, canMatch: [isAdmin] }` 2. `{ path: 'dashboard', component: UserDashboard }` An admin matches the first; everyone else falls through to the second. With `canActivate` in place of `canMatch`, the first route would match every user and non-admins would have their navigation cancelled instead of seeing the user dashboard. The same pattern serves feature flags and A/B variants. For an admin-only area, putting `canMatch` on the `admin` route has a further effect: a non-admin falls through to the `**` not-found route, so the app does not reveal that an admin section exists. Whether that is desirable (a 404 instead of a "forbidden" page) is a product decision. ## The currentSnapshot argument `canMatch` originally received only `route` and `segments`. Angular 21.2 added a third argument, `currentSnapshot`, of type `PartialMatchRouteSnapshot`, and v22 made it required in the `CanMatchFn` type and the `CanMatch` interface (a migration updates class implementations). It is a `Pick` of `ActivatedRouteSnapshot` holding what is known at matching time: - `routeConfig`, `url`, `params`, `paramMap`, `queryParams`, `queryParamMap`, `fragment`, `data`, `outlet`, `title`. - Not included: `parent`, `root`, `pathFromRoot`, `firstChild`, `children`, because the tree is not fully matched yet. - Resolved data and lazily loaded components are not available either, since resolvers and component loading happen later. This lets a `canMatch` guard decide on a path parameter (for example a tenant id) without parsing `segments` by hand. ## canMatch and lazy loading For a route with `loadChildren`, the router runs `canMatch` before it asks for the child config. If the guard rejects, this navigation never downloads that chunk. That is why `canMatch` replaced `canLoad`, which was deprecated in v15.1 and only ever blocked `loadChildren`. Two caveats keep this honest: - **Preloading ignores `canMatch`.** A preloading strategy such as `PreloadAllModules` skips `loadChildren` routes only when they declare `canLoad`; `canMatch` does not stop preloading. - **Chunks are not secrets.** Anyone can request a JavaScript file; code that must stay private must not ship to the browser, and data must be protected by the server. ## Common mistakes - **Expecting `canMatch` to cancel.** A team swaps `canActivate` for `canMatch` on `/admin` and is surprised that non-admins now see the not-found page instead of a sign-in redirect. If the intent is "redirect", return a `UrlTree` from `canMatch` rather than `false`. - **Forgetting the fallback.** With no later route and no `**` wildcard, a rejected `canMatch` leaves the URL unmatched, and the navigation fails with a no-match error instead of showing anything useful. - **Expensive checks.** `canMatch` may run for every navigation that reaches that point in the config, so cache the role or flag rather than calling the server on each evaluation. - **Reading parents from `currentSnapshot`.** Only the picked fields exist; parent and child routes are not part of the partial snapshot. ## Choosing between them - Use `canMatch` when rejection should mean "this route is not for you, find another": role-specific screens on one path, feature flags, hiding a section behind a not-found page. - Use `canActivate` when rejection should mean "stop, or go here instead" and the guard needs the full `ActivatedRouteSnapshot` and `RouterStateSnapshot`, including parent routes. - Both can redirect with a `UrlTree` or `RedirectCommand`, and both run in the route's injection context, so `inject()` works in either.

  • In Angular, does a canMatch guard on a loadChildren route stop PreloadAllModules from downloading that chunk?
    No. The router's preloader skips a `loadChildren` route only when the route declares `canLoad`; `canMatch` is evaluated during navigation matching, not during preloading. If a chunk must not be preloaded for some users, use a custom preloading strategy that checks the condition, and remember the file is still publicly fetchable either way.
  • In Angular, what happens when a canMatch guard returns a UrlTree instead of false?
    The router does not fall through to other routes. It treats the `UrlTree` (or a `RedirectCommand`) as a redirect, cancels the current navigation and starts a new navigation to the target, exactly as a redirect from `canActivate` would.

canMatch is a building directory that simply omits a floor you are not cleared for, so you follow the next listing that fits; canActivate is the locked door on the floor you already found, which just turns you away.

saying these in an interview costs you the question

  • canMatch returning false cancels the navigation just like canActivate does.
  • canMatch keeps a lazy chunk from ever being downloaded, including by preloading.
  • Two routes with the same path are a configuration error in Angular.
  • canLoad is still the recommended guard for lazy loadChildren routes.
  • canMatch receives the full RouterStateSnapshot with parent and child routes.