skip to content

Hierarchical Dependency Injection

Angular's injector tree: provider recipes and tokens, inject() and its context, environment vs element injectors, lookup modifiers and service scope. Interviewers probe where an instance comes from.

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

explore

questions

27

In Angular, what is the difference between injecting with inject() and injecting through constructor parameters, and which does the style guide recommend?

level: juniorimportance: must knowfreq 70%

answer

  1. same injector, same resolution
  2. explicit token vs parameter type
  3. inheritance without super arguments
  4. field order matters
  5. usable in plain functions

basics

~20 s

Both resolve dependencies from the same injector in the same way. inject() is called in a field initializer or constructor, giving better type inference, no super() forwarding and use in plain functions; Angular's style guide prefers it.

solid answer

~40 s

Constructor injection declares dependencies as parameters, `constructor(private cart: Cart) {}`, and the compiler turns the parameter types into a factory. `inject(Cart)` asks for the same thing explicitly, usually in a field initializer: `private cart = inject(Cart)`. Resolution is identical — same injector, same lookup — so the difference is ergonomics. `inject()` takes the token as an argument, so `InjectionToken`s need no `@Inject` decorator and the type is inferred; subclasses do not have to repeat and forward the base class's dependencies through `super(...)`; and functions such as guards and provider factories can call it, which constructors cannot help with. Angular's style guide recommends `inject()`. The catch is that it only works in an injection context, and field initializers run top to bottom, so a field that uses an injected dependency must be declared after it.

code

ts · 23 lines
ts
import { Component, Injectable, InjectionToken, inject, signal } from '@angular/core';

export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL', { factory: () => '/api' });

@Injectable({ providedIn: 'root' })
export class Cart {
  readonly items = signal<string[]>([]);
}

// Base class resolves its own dependencies.
export abstract class PageBase {
  protected readonly baseUrl = inject(API_BASE_URL); // typed as string, no @Inject
}

@Component({
  selector: 'app-checkout-page',
  template: `<p>{{ itemCount }} items, API at {{ baseUrl }}</p>`,
})
export class CheckoutPage extends PageBase {
  private readonly cart = inject(Cart); // injected fields first
  protected readonly itemCount = this.cart.items().length; // safe: cart already assigned
  // No constructor needed: nothing to forward to super().
}

go deeper

for a junior

Know both syntaxes, that they resolve the same way, and that the style guide prefers inject() in field initializers.

for a middle

Explain the practical advantages — token typing, no @Inject, inheritance without forwarding — and the declaration-order trap.

for a senior

Use inject() to simplify base classes and shared helpers while keeping calls inside construction time, and review for fields that read dependencies too early.

for a principal

Set one injection style for the codebase, schedule the mechanical migration, and make field-order and context rules part of review guidance.

## Two ways to ask for a dependency Angular's **dependency injection** (DI) creates components, directives and services and supplies what they need. A class can state its needs in two ways. **Constructor parameters** — the classic form: ```ts export class CheckoutPage { constructor(private readonly cart: Cart, @Inject(API_BASE_URL) private readonly baseUrl: string) {} } ``` The Angular compiler reads the parameter types and generates a factory that resolves each one. A parameter whose type is not a usable token — a string resolved through an `InjectionToken`, for instance — needs the `@Inject(TOKEN)` parameter decorator. **The `inject()` function** — the current recommendation: ```ts export class CheckoutPage { private readonly cart = inject(Cart); private readonly baseUrl = inject(API_BASE_URL); // typed as string } ``` Both forms are resolved by the same injector with the same rules. The style guide says `inject()` "works the same way as constructor parameter injection"; the reasons to prefer it are about code, not behaviour. ## Why the style guide prefers `inject()` | Concern | Constructor parameters | `inject()` | |---|---|---| | Token that is not a class | needs `@Inject(TOKEN)` | pass the token directly | | Type of the result | written by hand | inferred from the token | | Base class with dependencies | subclass must accept and forward them via `super(...)` | base class resolves its own; subclass calls `super()` with no DI arguments | | Comments per dependency | awkward inside a parameter list | natural on each field | | Fields computed from a dependency | may need separate declaration and assignment | can be initialized inline, after the injected field | | Plain functions (guards, factories) | not applicable | works in their injection context | The inheritance row is often the deciding one in real codebases: adding a dependency to a base class with constructor injection forces edits to every subclass constructor. ## Same moment, same injector It helps to see why the two forms behave identically. For a class with constructor parameters, the compiler generates a factory that, while Angular is creating the instance, asks the current injector for each parameter's token and passes the results to `new`. A field initializer that calls `inject()` runs during that same creation, with that same injector current. For a component, that is its element injector, falling back through ancestor elements to the environment injectors. Nothing about lookup rules, scoping or instance sharing changes between the forms — which is why switching styles is a mechanical refactor rather than a behavioural change. ## The rules that come with `inject()` 1. **Injection context only.** `inject()` works while Angular is creating the instance — in field initializers and in the constructor body — and in other injection contexts such as provider factories. Calling it later, in a lifecycle hook or event handler, throws NG0203. 2. **Declaration order.** Field initializers run top to bottom. In ```ts total = this.cart.items().length; // runs first: this.cart is undefined private readonly cart = inject(Cart); ``` the first line fails with a `TypeError`, not a DI error — the context is fine, the field is just not assigned yet. Put injected fields first. 3. **Explicit tokens.** Because the token is an argument, refactors that change a parameter's type no longer silently change what is injected. ## When you still meet constructor injection Most production code written before `inject()` became the norm uses constructor parameters, and it remains fully supported for classes decorated with `@Injectable`, `@Component`, `@Directive` and `@Pipe`. The v22 `@Service()` decorator is designed around `inject()`; its guide lists constructor-based DI as unsupported. You can mix both forms in one class, though a consistent style is easier to read. Converting an existing codebase is a mechanical migration with its own tooling. ## Practical guidance - Default to `inject()` in field initializers, injected fields first. - Keep constructors for logic that needs several injected values together, or drop them entirely. - In base classes, prefer `inject()` so subclasses stay free of DI plumbing. - Treat NG0203 as a sign that `inject()` moved out of construction time, not as a reason to go back to constructor parameters.

  • Is inject() slower or faster than constructor injection at runtime?
    Neither in any way that matters: both end up asking the same injector for the same token while the instance is being created. The choice is about readability, typing and inheritance, which is why the style guide frames its recommendation as style advantages rather than performance.
  • A subclass constructor calls super() and then uses a field the base class set with inject(). Is that safe?
    Yes. Base-class field initializers run during the `super()` call, still inside the injection context of the subclass's creation, so by the time the subclass constructor body continues the base fields are assigned. Only reading a field before its own initializer has run is a problem.

saying these in an interview costs you the question

  • inject() resolves from a different injector than constructor parameters
  • inject() can be called anywhere in a component, including ngOnInit
  • Angular reorders fields so injected ones are always ready first
  • Subclasses must forward the base class's inject() dependencies to super()
  • Constructor injection is removed in current Angular
open as a page

In Angular, what do the provider recipes useClass, useValue, useFactory and useExisting each tell the injector to return?

level: juniorimportance: must knowfreq 72%

basics

~20 s

An Angular provider pairs a token with a recipe: useClass instantiates a class, useValue returns a fixed value untouched, useFactory returns what a function builds, and useExisting aliases another token. A bare class is shorthand for useClass of itself.

open as a page

In Angular, how does a service declared with providedIn: 'root' differ from one listed in a component's providers array in instance count and lifetime?

level: juniorimportance: must knowfreq 76%

basics

~20 s

providedIn: 'root' gives one application-wide instance, created lazily on first injection and kept until the app is destroyed. A component's providers array creates a separate instance for each component instance, visible to its subtree and destroyed with it.

open as a page

In Angular, why can't a TypeScript interface serve as a DI token, and how does an InjectionToken solve that for an API_BASE_URL string?

level: juniorimportance: must knowfreq 62%

basics

~20 s

TypeScript interfaces are erased at compile time, so there is nothing at runtime for the injector to use as a key. An InjectionToken<T> is a real object that serves as that key, and its type parameter makes inject() return T.

open as a page

In Angular, where does an injection context exist, and why does calling inject() inside ngOnInit throw NG0203?

level: middleimportance: must knowfreq 60%

basics

~20 s

An injection context exists while Angular creates a DI-managed instance (field initializers, constructor), inside provider and InjectionToken factories, and in functions the framework runs in context, such as guards. ngOnInit runs after construction, when no context is active, so inject() throws NG0203.

open as a page

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%

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.

open as a page

In Angular, how do the self, skipSelf and host options of inject() change where a dependency lookup starts and stops?

level: middleimportance: must knowfreq 58%

basics

~20 s

skipSelf starts the search at the parent instead of the current element; self checks only the current element's injector; host stops at the host of the template the requester is declared in. self and host never fall back to environment injectors.

open as a page

In an Angular app, the header's cart badge stays at zero while the lazy checkout route shows items — how can CartService end up with two instances, and how do you fix it?

level: seniorimportance: must knowfreq 62%

basics

~20 s

CartService is also registered below root — in the checkout route's providers, a lazily loaded NgModule's providers, a component's providers, or via the deprecated providedIn: 'any' — so checkout gets its own instance while the header uses root's. Remove that extra registration.

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 Angular, what does inject(Token, {optional: true}) return when no injector provides the token, and how does that differ from a plain inject() call?

level: juniorimportance: should knowfreq 45%

basics

~20 s

With optional: true, Angular returns null when the whole search finds no provider; a plain inject() throws NG0201 instead. The option changes only the not-found outcome, not where Angular searches, and its constructor equivalent is @Optional().

open as a page

In Angular, how do you write a reusable injectX() helper function, and what do assertInInjectionContext and runInInjectionContext each add?

level: middleimportance: should knowfreq 32%

basics

~20 s

An injectX() helper is a plain function that calls inject() and must itself be called in an injection context. assertInInjectionContext makes misuse fail with the helper's name; runInInjectionContext lets context-less code call it through a captured injector.

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

In Angular, how should a nested accordion component inject its parent accordion, and why does that lookup need skipSelf and optional?

level: middleimportance: should knowfreq 36%

basics

~10 s

Use inject(Accordion, {skipSelf: true, optional: true}). Without skipSelf the lookup finds the accordion on its own element and throws NG0200; without optional the outermost accordion, which has no parent, throws NG0201.

open as a page

In Angular, what is the difference between { provide: A, useExisting: B } and { provide: A, useClass: B } when B is also provided?

level: middleimportance: should knowfreq 58%

basics

~20 s

With useExisting, token A is an alias: injecting A resolves B and returns B's instance, so both tokens share one object. With useClass, the injector builds a second, independent B instance for A, so the two tokens hold separate state.

open as a page

In Angular, how does a useFactory provider get its dependencies, and when does the injector actually call the factory?

level: middleimportance: should knowfreq 45%

basics

~20 s

A useFactory function receives dependencies either positionally from its deps array or by calling inject() inside its body, since it runs in an injection context. The injector calls it lazily on the first request and caches the result for that injector.

open as a page

In Angular 22, what does the @Service() decorator do, and when should you still reach for @Injectable instead?

level: middleimportance: should knowfreq 42%

basics

~20 s

@Service() marks a class as a tree-shakable root singleton, shorthand for @Injectable({providedIn: 'root'}). It supports a factory option and autoProvided: false, but no constructor injection, useClass-style keys or platform scope — use @Injectable for those.

open as a page

What makes an Angular provider tree-shakable, and why is providedIn: 'root' tree-shakable while a providers-array entry is not?

level: middleimportance: should knowfreq 48%

basics

~20 s

A provider is tree-shakable when the service registers itself (providedIn: 'root' or @Service()), so only code that injects it references the class. A providers-array entry references the class from configuration, so the bundler must keep it even if nothing injects it.

open as a page

In Angular, when is an abstract class a better DI token than an InjectionToken or a concrete class, and why are string tokens discouraged?

level: middleimportance: should knowfreq 40%

basics

~20 s

An abstract class exists at runtime, so it can be a token, and it doubles as the type contract implementations extend, without shipping an implementation. InjectionToken suits non-class values. String tokens lose type safety, can collide, and inject() does not accept them.

open as a page

In Angular, what does giving an InjectionToken a factory do, and how does an explicit provider for the same token interact with it?

level: middleimportance: should knowfreq 44%

basics

~20 s

A factory makes an InjectionToken self-providing: it behaves as if registered in the root injector, so it never needs a providers entry and is tree-shakable. Any explicit provider found first on the lookup path overrides that default.

open as a page

An Angular helper function calls inject() inside a setTimeout callback and throws NG0203 when the timer fires; why, and how do you fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

An injection context lasts only for the synchronous call that created it; the setTimeout callback runs later on a fresh stack with no current injector. Call inject() before scheduling, or capture an Injector and use runInInjectionContext.

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

In Angular, what does multi: true do on a provider, and why can a component-level multi provider hide contributions registered at the root?

level: seniorimportance: should knowfreq 40%

basics

~20 s

multi: true makes a token collect every contribution registered in one injector into an array. Arrays do not merge across injectors: a component that adds its own multi entry creates a new array, hiding the root's entries from its descendants.

open as a page

In Angular, how would you design a typed configuration-object token for an API client so that defaults apply and deployments override only what they need?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Declare one InjectionToken for a readonly config interface, give it a factory that returns complete defaults, and expose a small provider function that merges a partial override with those defaults into a full object, so consumers always inject a complete, typed configuration.

open as a page

In Angular, why would a provider reference a class through forwardRef(() => SomeClass), and what problem does it not solve?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

forwardRef wraps a class reference in a closure so DI metadata can mention a class not yet defined where that metadata is evaluated; Angular calls the closure later. It fixes declaration order, not a cycle where two services inject each other.

open as a page

In Angular 22, what does injectAsync() do, and why must the service it loads be auto-provided?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

injectAsync(() => import('./x')) returns a function that, when called, loads the service's chunk and resolves it from DI. The service must be providedIn: 'root' or @Service() because no providers array can reference a class that has not been loaded.

open as a page

An Angular accordion item using inject(Accordion, {host: true}) works inside <app-accordion> but throws NG0201 once moved into a wrapper component's template — why, and how do you fix it?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

host limits the lookup to the template declaring the item. In a wrapper's template the boundary becomes the wrapper, exposing only itself and its viewProviders, so the outer accordion is unreachable. Drop host, or re-expose it via viewProviders.

open as a page

In an Angular component library, what is the lightweight injection token pattern, and why does it let an unused component be tree-shaken?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

A library component that queries or injects another component by class keeps that class in the bundle even when unused. The pattern swaps the class for a small abstract token the optional component provides via useExisting, so only the tiny token is retained.

open as a page