In Angular, what distinguishes environment injectors from element injectors, and which configuration places a provider in each?
answer
- two trees, not one
- application and route level vs DOM level
- ApplicationConfig, route providers, root
- component and directive providers arrays
basics
~10 sEnvironment 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.
solid answer
~40 sAngular keeps two injector hierarchies. **Environment injectors** form a chain from the platform injector through the `root` injector (configured by `bootstrapApplication`'s `ApplicationConfig.providers` plus every `providedIn: 'root'` or, in v22, `@Service()` class) down to child injectors the router creates for routes with `providers` and ones you create with `createEnvironmentInjector()`. **Element injectors** belong to DOM elements that host a component or directive; they are empty unless that `@Component` or `@Directive` lists `providers` or `viewProviders`, and a service provided there is created per host instance and destroyed with it. Environment-only providers such as `provideHttpClient()` return `EnvironmentProviders`, which Angular rejects in a component's `providers` with NG0207.
code
ts · 35 linesimport {Component, Service, inject} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {provideHttpClient} from '@angular/common/http';
import {provideRouter, Routes} from '@angular/router';
@Service() // root environment injector, tree-shakable
export class ApiClient {}
@Service({autoProvided: false})
export class AdminAudit {}
@Service({autoProvided: false})
export class DraftStore {}
@Component({
selector: 'app-editor',
providers: [DraftStore], // element injector: one DraftStore per <app-editor>
template: `<p>editor</p>`,
})
export class Editor {
private drafts = inject(DraftStore);
private api = inject(ApiClient);
}
const routes: Routes = [
// child environment injector created by the router for this route
{path: 'admin', providers: [AdminAudit], loadChildren: () => import('./admin.routes')},
];
@Component({selector: 'app-root', imports: [Editor], template: `<app-editor />`})
export class App {}
bootstrapApplication(App, {
providers: [provideHttpClient(), provideRouter(routes)], // root environment injector
});go deeper
Name the two kinds of injector and one way to put a provider in each: ApplicationConfig or providedIn root for the environment side, a component's providers array for the element side.
Explain the environment chain from NullInjector through platform and root to route injectors, and why element-provided services are per host instance and die with it.
Show you choose the injector deliberately: root for infrastructure, route providers for feature state, component providers for per-instance state, and know why EnvironmentProviders are rejected on components.
Discuss how injector placement shapes a large app's ownership boundaries: which team owns root-level services, and how route-level injectors keep features isolated and lazily loaded.
## Two hierarchies, not one Angular's dependency injection is often described as "a tree of injectors", but in current Angular (standalone components, Angular 22.2) there are really **two separate trees** that a lookup can visit: | | Environment injectors | Element injectors | |---|---|---| | **Where they live** | Application, platform, routes, manually created | One per element that hosts a component or directive | | **Configured by** | `ApplicationConfig.providers`, `providedIn: 'root'` / `@Service()`, `Route.providers`, `createEnvironmentInjector()` | `providers` and `viewProviders` on `@Component`, `providers` on `@Directive` | | **Lifetime** | Application, or as long as the route config / manual injector lives | As long as the host element's component or directive instance | | **Accepts `EnvironmentProviders`** | Yes | No (NG0207) | A **provider** is the recipe that tells an injector how to create the value for a **token** (a class or an `InjectionToken`). An **injector** holds providers and caches the instances it creates, so every provider you write must sit in one of these two kinds of injector. ## The environment injector chain The environment hierarchy is a short, mostly static chain: 1. **`NullInjector`** — the top. It holds nothing; reaching it without a match throws `NG0201` (or returns `null` for an optional lookup). 2. **Platform injector** — created by the platform (`platformBrowser()` in a browser). It holds platform-wide services and anything declared `providedIn: 'platform'`; several applications on one page can share it. 3. **`root` environment injector** — created by `bootstrapApplication(App, appConfig)`. It is configured by `appConfig.providers` (where `provideRouter()`, `provideHttpClient()` and similar functions go) and it also lazily owns every class marked `@Injectable({providedIn: 'root'})` or, since v22, `@Service()`. 4. **Child environment injectors** — the router creates one for a route that declares `providers` (used by that route, its children, its guards and resolvers), and you can create one yourself with `createEnvironmentInjector(providers, parent)` for dynamically created components or plugin-style features. Environment injectors are the only place where `EnvironmentProviders` — the opaque type returned by `provideHttpClient()`, `provideRouter()`, `importProvidersFrom()` and similar functions — may appear. ## Element injectors Every element in a template that hosts a component or directive can carry an **element injector**. It is empty by default; it gains providers only when the component or directive declares them: - `providers` on `@Component` or `@Directive` — visible to the component itself, its template, and content projected into it. - `viewProviders` on `@Component` — visible only inside the component's own view, not to projected content. - Components and directives on the same element **share** one element injector. - Each host instance gets its **own** instance of the provided service, created when first injected and destroyed with the host. That last point is why a per-instance service — a wizard's form state, an editor's undo stack — is provided on the component: every wizard on the page gets its own state, and navigating away cleans it up. ## Choosing where a provider goes - App-wide, stateless or singleton infrastructure: `providedIn: 'root'` / `@Service()` (tree-shakable) or `ApplicationConfig.providers` for configuration functions. - Feature-scoped state shared by a route and its children: the route's `providers`. - Per-component-instance state: the component's `providers`. - Framework configuration functions (`provideHttpClient()`, `provideRouter()`): only environment injectors; putting them in a component throws NG0207 ("EnvironmentProviders in wrong context"). ## Why the split matters The two trees are searched in a fixed order — element injectors first, then environment injectors — so knowing which kind of injector holds a provider tells you **who can see it** and **how long the instance lives**. A service in `root` can never see a component's element providers, and a service in a component's `providers` is never a singleton. Most "why do I have two instances" and "why is there no provider" questions reduce to putting a provider in the wrong one of these two trees. ## Misconceptions worth correcting - **"Every element has a full injector."** Element injectors are created implicitly but hold nothing unless a component or directive on that element declares providers; a plain `<div>` contributes nothing to a lookup. - **"`providedIn: 'root'` creates the service at startup."** Root-provided classes are created lazily, on first injection, and are dropped from the bundle if never injected. - **"Route providers belong to the route's component."** They live in an environment injector the router creates for the route, shared by that route's children, guards and resolvers, and independent of any one component instance. - **"NgModule apps have a different system."** They use the `ModuleInjector` built from `@NgModule` providers; it plays the environment injector's role and the element side is identical.
- Why does Angular throw NG0207 when provideHttpClient() is placed in a component's providers array?`provideHttpClient()` returns `EnvironmentProviders`, a type Angular only accepts in environment injectors: `ApplicationConfig.providers`, a route's `providers`, or `createEnvironmentInjector()`. Element injectors accept plain `Provider` objects only, so the framework rejects it with NG0207 (EnvironmentProviders in wrong context). The design keeps app-level infrastructure such as interceptors and the router out of per-element scopes.
- What sits above the root environment injector, and what is it for?The platform injector, created by the platform function such as `platformBrowser()`, holds platform-wide services and anything `providedIn: 'platform'`, and can be shared by several applications bootstrapped on one page. Above it is the `NullInjector`, which holds nothing and throws NG0201 for any token that reaches it, unless the lookup is optional.
A company has a head office and a branch office for each department (environment injectors), plus a desk drawer at each employee's desk (element injectors). Staff check their own drawer and their manager's drawers first, then go to the department office and finally head office.
saying these in an interview costs you the question
- There is a single injector per application
- Every DOM element gets a populated injector with all services
- A service in a component's providers is still an app-wide singleton
- provideHttpClient() can be listed in a component's providers
- providedIn: 'root' creates the instance eagerly at bootstrap
- Route providers live in the route component's element injector