skip to content

Injector Hierarchy

Angular resolves a token by walking element injectors up the view tree, then environment injectors from lazy route to root and platform. Interviewers ask about NG0201 and viewProviders.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Angular, when a component injects a token, in what order are injectors searched, and what happens when none provides it?

level: middleimportance: must knowfreq 72%

answer

  1. two phases, fixed order
  2. element ancestors before environment
  3. first match wins, no merging
  4. NullInjector at the very top
  5. NG0201 plus a dependency path

basics

~20 s

Angular first walks element injectors from the requesting element up through its ancestors, then walks environment injectors from the component's own (route or root) up to platform. The first provider found wins; reaching NullInjector throws NG0201.

solid answer

~40 s

Resolution has two phases. First Angular checks the **element injector** of the requesting element, then each ancestor element's injector up the view tree, including across component boundaries. If none has a provider, it goes back to the requesting component and walks the **environment injectors**: the injector the component was created with (a route's child injector for a routed component, otherwise `root`), then its parents up to `root`, the platform injector and finally the `NullInjector`, which throws `NG0201: No provider found for ...` with a path such as `Path: App -> AuthClient -> UserClient`. The **first** injector with a provider wins; Angular never merges or checks for a closer duplicate afterwards. A service created in an environment injector resolves its own dependencies only from that environment injector upwards, never from element injectors.

code

ts · 22 lines
ts
import {Component, Service, inject} from '@angular/core';

@Service() // root: the fallback everyone can reach
export class WizardState {
  label = 'root';
}

@Component({selector: 'app-step', template: `<p>{{ state.label }}</p>`})
export class Step {
  state = inject(WizardState); // walks element injectors first
}

@Component({
  selector: 'app-wizard',
  imports: [Step],
  providers: [{provide: WizardState, useFactory: () => ({label: 'wizard'})}],
  template: `<app-step />`, // renders 'wizard': closest provider wins
})
export class Wizard {}

@Component({selector: 'app-root', imports: [Wizard, Step], template: `<app-wizard /><app-step />`})
export class App {} // the second <app-step> has no element provider, so it gets root's 'root'

go deeper

for a junior

Remember the order: the component's own providers, its ancestors' providers, then route and root, and an error if nothing matched.

for a middle

Walk through both phases precisely, explain that the first match shadows the rest, and read an NG0201 path to find the token nobody provides.

for a senior

Diagnose why a root-provided service cannot see element providers, and why routed or dynamically created components reach different environment injectors than their neighbours.

for a principal

Weigh how much the team should lean on shadowing for per-subtree behaviour versus explicit inputs, since implicit closest-provider rules make ownership harder to see in review.

## The two-phase walk When code in a component or directive calls `inject(SomeToken)` (or declares it as a constructor parameter), Angular resolves the **token** by searching two injector trees in a fixed order: 1. **Element phase.** Start at the element injector of the element that hosts the requesting component or directive. If it has no provider, move to the parent element's injector, and keep going up — through the host elements of enclosing components too, since a component's host element sits inside its parent's template. 2. **Environment phase.** If no element injector matched, go back to the requesting component and ask the **environment injector it was created with**. That is a route's child injector when the component was activated by a route with `providers` (or a lazily loaded child), the injector passed to `createComponent`, or the `root` injector otherwise. Then walk that injector's parents: parent route injectors, `root`, the platform injector. 3. **NullInjector.** At the very top, the `NullInjector` holds nothing. Reaching it throws `NG0201` — unless the lookup was optional, in which case it yields `null`. | Step | Injector | Typical providers | |---|---|---| | 1 | Requesting element | the component's own `providers` / `viewProviders` | | 2 | Ancestor elements | parent components' and directives' `providers` | | 3 | Component's environment injector | route `providers`, `createEnvironmentInjector()` output | | 4 | Parent environment injectors | parent routes, then `root` (`ApplicationConfig`, `@Service()`, `providedIn: 'root'`) | | 5 | Platform | `providedIn: 'platform'`, platform services | | 6 | `NullInjector` | nothing — throws NG0201 | ## First match wins The walk stops at the **first** injector that has a provider for the token. A closer provider **shadows** a farther one: - A wizard component that lists `WizardState` in `providers` gives every descendant step that instance, even if `WizardState` is also provided in `root`. - There is no merging: for an ordinary provider, a token is answered by exactly one injector or by none. This shadowing is the mechanism behind per-subtree instances, and also behind many surprises: a descendant reading "the wrong" instance usually has a closer provider on its path. ## What the error tells you In Angular 22 the failure reads like `NG0201: No provider found for \`UserClient\`.`, followed by details such as `Source: ...` and `Path: App -> AuthClient -> UserClient`. Older versions printed `NullInjectorError: No provider for UserClient!`, a name you still meet in search results. The path is the useful part: it names the chain of injections that led to the missing token, so you debug **the last entry** — the token nobody provides from where it was requested. Common causes: - The class has no `@Service()` / `providedIn: 'root'` and is not listed in any reachable `providers`. - The provider exists, but in an injector **off the path** — a sibling component's `providers`, or a route that is not an ancestor of where the component was created. - An environment-injector service asks for an element-level token. ## Services resolve from their own injector The element phase only exists for components and directives. A service instance created by the **root** injector resolves its own dependencies from `root` upward. So if a `providedIn: 'root'` service injects `WizardState`, which is only provided on a wizard component, it fails with NG0201 even though the component that first injected the service could see `WizardState`. The service belongs to where it was *provided*, not to who asked for it first. ## A worked trace Take a routed `CheckoutPage`, activated by a route `checkout` that declares `providers: [Pricing]`; its template renders `<app-wizard>`, whose template renders `<app-step>`. When `Step` injects three tokens: 1. `WizardState` — `Step`'s own element injector has nothing; the `<app-wizard>` host has it in `providers`: **found at step 2 of the walk**. 2. `Pricing` — no element injector along `<app-step>`, `<app-wizard>`, `<app-checkout-page>` and the outlet's ancestors has it; the component's environment injector (the `checkout` route's) does: **found in the environment phase**. 3. `Analytics` — declared with no `providedIn` and listed nowhere: every element injector, the route injector, `root` and platform miss it, and the `NullInjector` throws **NG0201** with a path ending in `Analytics`. ## Changing the walk The walk can be narrowed or made optional with lookup options on `inject()` — restricting it to the current element, skipping it, stopping at the host, or returning `null` instead of throwing. Those modifiers change where the search starts or stops; the order described here is the baseline they modify.

  • A routed Angular component injects a service listed in a parent component's providers, and the parent hosts the router-outlet. Does it find it?
    Yes. The router creates the routed component inside the outlet's view container, so its element-injector walk continues up through the outlet's ancestors, including the parent component's element injector. Only after that does it switch to the route's environment injector and then `root`.
  • Why does a providedIn root service fail with NG0201 when it injects a service provided on a component?
    The root injector creates the service, so its dependencies are resolved from the root environment injector upwards. Element injectors are never consulted for it, regardless of which component first requested the service. Either provide the dependency in an environment injector, or pass the value in as a method argument instead of injecting it.
  • How do you read the path in an NG0201 message?
    The `Path:` lists the injection chain from the first requester to the missing token, for example `App -> AuthClient -> UserClient`. The last entry is the token that no reachable injector provides; the entries before it show who asked, which tells you whose injector context the lookup ran in.

saying these in an interview costs you the question

  • Angular searches root first, then component providers
  • All matching providers are merged along the path
  • A root service can inject providers from the requesting component
  • Element injectors stop at the component boundary
  • A missing provider silently injects undefined
  • Environment injectors are searched before element injectors
open as a page

In Angular, what distinguishes environment injectors from element injectors, and which configuration places a provider in each?

level: juniorimportance: should knowfreq 58%

basics

~10 s

Environment injectors hold application-wide and route-level providers (root, platform, route providers, createEnvironmentInjector). Element injectors exist per element and hold only providers declared on that element's components and directives, living as long as that element.

open as a page

In an Angular wizard component that receives its steps through <ng-content>, what changes when its form-state service moves from providers to viewProviders?

level: middleimportance: should knowfreq 45%

basics

~20 s

viewProviders are visible only inside the component's own template, so projected step components can no longer see the wizard's form state: they get NG0201, or silently the root instance if the service is also root-provided.

open as a page

An Angular dialog, opened from a lazy route with createComponent, throws NG0201 for a service in that route's providers, though the route's components inject it fine — why, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~10 s

The dialog was created with the root environment injector (for example ApplicationRef.injector), which cannot see the route's child injector. Pass the route's EnvironmentInjector (inject(EnvironmentInjector) in the opener) to createComponent, or provide the service higher.

open as a page