skip to content

In Angular's router, when do resolvers run relative to guards, in what order across parent and child routes, and which emitted value is kept?

level: middleimportance: should knowfreq 45%

answer

  1. guards first, then resolve phase
  2. one route at a time, top-down
  3. keys on one route side by side
  4. first(), not last value

basics

~20 s

Resolvers run only after every guard in the navigation has passed; routes are resolved parent before child, one route at a time, the resolvers on a single route run concurrently, and the router keeps only each resolver's first emitted value.

solid answer

~40 s

The Angular router checks all guards for the target tree first, and only if they all pass does it enter the resolve phase, emitting `ResolveStart`. It then walks the routes that need resolving top-down: a parent route's resolvers finish before a child's start, so a child resolver can read `route.parent?.data`. Within one route, the entries of the `resolve` map run concurrently. For an Observable result the router takes the **first** emission and unsubscribes; since v14 it no longer waits for completion. If a resolver completes without emitting, the navigation is cancelled with the `NoDataFromResolver` cancellation code, and if it never emits, the navigation hangs.

code

ts · 25 lines
ts
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, Routes } from '@angular/router';
import { CustomerApi, InvoiceApi, TaxApi, Customer } from './api';

export const routes: Routes = [
  {
    path: 'customers/:customerId',
    // runs first
    resolve: { customer: (r: ActivatedRouteSnapshot) => inject(CustomerApi).get(r.paramMap.get('customerId')!) },
    children: [
      {
        path: 'invoices/:id/edit',
        loadComponent: () => import('./invoice-edit').then((m) => m.InvoiceEdit),
        // these two start together, after the parent's customer has resolved
        resolve: {
          invoice: (r: ActivatedRouteSnapshot) => {
            const customer = r.parent?.data['customer'] as Customer;
            return inject(InvoiceApi).get(customer.id, r.paramMap.get('id')!);
          },
          taxRates: () => inject(TaxApi).currentRates(),
        },
      },
    ],
  },
];

go deeper

for a junior

Remember that guards decide first and resolvers fetch afterwards, and that the router uses the resolver's first value.

for a middle

Explain the parent-to-child sequence, the concurrency within one route, and what happens when a resolver completes empty or never emits.

for a senior

Diagnose hung or cancelled navigations from resolver streams, and restructure routes to avoid a resolver waterfall between independent requests.

for a principal

Weigh the waterfall that nested resolvers create against the clarity they give, and decide which data dependencies justify blocking a navigation.

## Where resolving sits in a navigation An Angular navigation is a pipeline. Simplified, the router: 1. matches the URL against the route configuration (this is where `canMatch` guards run); 2. runs the **guard phase** for the whole target tree: `canDeactivate` on routes being left, then `canActivateChild` and `canActivate` on routes being entered; 3. runs the **resolve phase**, bracketed by the `ResolveStart` and `ResolveEnd` router events; 4. loads any lazy components, activates the routes and creates the components. The consequence is simple and often asked: **a resolver never runs if a guard rejects the navigation**. The router's own documentation gives the order for a parent with `baseGuard` and `baseDataResolver` and a child with `childGuard` and `childDataResolver`: `baseGuard`, `childGuard`, `baseDataResolver`, `childDataResolver`. So a resolver can assume the user is authorised, and a guard cannot rely on resolved data. ## Order across routes: a waterfall by design Inside the resolve phase, the router processes the routes that need resolving **one route at a time, parent before child**. Internally each route is resolved with `concatMap`, so the next route's resolvers start only when the previous route's resolvers are done. This ordering is a feature: a child resolver can read what its parent resolved. For example a `customers/:customerId` parent resolves the customer, and the `invoices/:id/edit` child reads `route.parent?.data['customer']` to check the invoice belongs to it. It is also a cost. If the parent's resolver takes 200 ms and the child's takes 300 ms, the click is blocked for about 500 ms even when the two requests are independent. The router documentation calls this a network waterfall and points to route `resources`, which run concurrently across the matched routes. ## Order inside one route: concurrent The entries of a single route's `resolve` map are started together (the router uses `mergeMap` over the keys). With `resolve: { invoice: invoiceResolver, taxRates: taxRatesResolver }`, both requests are in flight at the same time and the route is done when the slower one yields a value. There is **no ordering guarantee** between them, so one resolver on the same route must not depend on another's result. If two of them return a `RedirectCommand`, only the first one returned is used. ## Which value is kept | Resolver returns | What the router uses | |---|---| | a plain value | that value | | a `Promise` | the fulfilled value | | an `Observable` that emits | the **first** emission; it then unsubscribes | | an `Observable` that completes without emitting | nothing: the navigation is cancelled with `NavigationCancellationCode.NoDataFromResolver` | | an `Observable` that never emits | nothing: the navigation waits until it emits or a newer navigation supersedes it | | a `RedirectCommand` | the navigation is redirected | The first-value rule changed in **Angular 14**. Before that, the router waited for the Observable to **complete** and took its **last** value. Old code that returned a never-completing store selector worked only by accident after v14 and hung before it; old code that relied on the last of several emissions silently changed meaning. A candidate who says "the resolver's Observable must complete" is describing pre-v14 behaviour. ## What the child finally sees After each route resolves, the router merges the results into that route's `data`. With the default `paramsInheritanceStrategy` of `'always'` (the default since v22; it was `'emptyOnly'` before), a child route's data also carries its parent's resolved values, with the child's own resolved keys winning on a clash. So `InvoiceEdit` under the customer route can read both `data['customer']` and `data['invoice']` from its own `ActivatedRoute`. Under `'emptyOnly'` it would inherit only when the child path is empty or the parent is componentless, which is why older code often walks `route.parent` explicitly. ## Practical rules that follow - Make resolvers return something that emits once: an `HttpClient` call, a `Promise`, or a store read piped through `take(1)` or `filter(Boolean)` plus `take(1)`. - Never return `EMPTY` to mean "no data"; it cancels the navigation with no error event and no redirect. - Put independent data on the **same** route so it loads concurrently, and reserve parent-child resolving for genuine dependencies. - Guard logic ("may this user open this invoice?") belongs in a guard, which runs first; do not fold authorisation into a resolver that runs only after the guards. - Add a timeout to slow resolvers so a stalled request cannot pin the navigation forever.

  • A resolver returns a store selector that emits undefined first and the loaded invoice later. What happens?
    The router takes the first emission, so the route activates with `invoice` set to `undefined` and the later value never reaches route data. Filter the stream to the loaded state and take one value, for example `filter(Boolean)` then `take(1)`, or trigger the load and wait for it before emitting.
  • How do you remove the parent-child waterfall when the child does not need the parent's data?
    Move the independent resolver onto the same route as the other so both start together, or fetch it in the component and render a placeholder. In Angular 22.2, route `resources` enabled with `withRouterResources()` run concurrently across all matched routes, though that API is still in developer preview.

saying these in an interview costs you the question

  • Resolvers and guards run in parallel to save time
  • A resolver's Observable must complete before navigation continues
  • The router uses the last value the resolver's Observable emits
  • Resolvers on the same route run in declaration order
  • Returning EMPTY from a resolver skips it and activates the route