Skip to content
Angular ng services 4 min read

Hierarchical Injectors

Angular’s dependency injection system is not a single flat registry — it is a tree of injectors that mirrors the structure of your application. When a class asks for a dependency, Angular walks up this tree until it finds a provider that can satisfy the request. Understanding this hierarchy is what lets you decide whether a service is a shared singleton or a fresh instance per component, and resolution modifiers like @Optional and @SkipSelf give you fine-grained control over exactly where the search starts and stops.

The two injector hierarchies

Angular maintains two parallel injector trees that work together during resolution:

  • The EnvironmentInjector hierarchy (the module/environment tree). This is rooted at the application’s root environment injector, created by bootstrapApplication. Providers registered with providedIn: 'root', in bootstrapApplication’s providers array, or via lazy-loaded route providers live here. It is concerned with application-wide and route-scoped services.
  • The ElementInjector hierarchy (the DOM/component tree). Every component and directive that declares a providers (or viewProviders) array gets its own element injector, structurally arranged like the rendered DOM. This is where you get per-component instances.

When a component requests a dependency, Angular first searches the element injector chain (the component, then its parent elements), and only if nothing is found there does it fall through to the environment injector chain. If neither tree can satisfy the token, Angular throws a NullInjectorError.

NullInjectorError: No provider for HttpClient!

How lookups bubble up the tree

Resolution proceeds from the most specific injector toward the root. Consider a service requested inside a deeply nested component:

import { Component, Injectable, inject } from '@angular/core';

@Injectable()
export class PanelService {
  readonly id = Math.random().toString(36).slice(2, 8);
}

@Component({
  selector: 'app-panel',
  standalone: true,
  providers: [PanelService], // new instance at THIS element injector
  template: `<p>Panel {{ service.id }}</p>`,
})
export class PanelComponent {
  readonly service = inject(PanelService);
}

Because PanelComponent declares PanelService in its providers, every instance of PanelComponent — and any child component beneath it — resolves to the same PanelService instance scoped to that subtree. A sibling PanelComponent elsewhere in the DOM gets its own copy. Move the provider up to a parent and the children share it; move it to providedIn: 'root' and the entire app shares one instance.

Tip: providers makes a service visible to a component, its children, and its content-projected children. viewProviders restricts visibility to the component’s own view (its template) and excludes projected content — useful for hiding internal helpers from consumers.

Resolution modifiers

You can change the default “search up to the root” behavior with decorators (for constructor injection) or option flags (for the inject() function). Both forms map to the same underlying flags.

Modifierinject() optionEffect
@Self(){ self: true }Only look in the current element injector; do not bubble up.
@SkipSelf(){ skipSelf: true }Skip the current injector and start the search at the parent.
@Host(){ host: true }Stop the search at the host element of the current component.
@Optional(){ optional: true }Return null instead of throwing if no provider is found.

These combine. A common pattern is an optional parent service lookup:

import { Component, inject } from '@angular/core';
import { PanelService } from './panel.service';

@Component({
  selector: 'app-child',
  standalone: true,
  template: `<span>{{ parentId }}</span>`,
})
export class ChildComponent {
  // Skip our own injector, look only at the parent's host, allow missing.
  private readonly parent = inject(PanelService, {
    skipSelf: true,
    host: true,
    optional: true,
  });

  readonly parentId = this.parent?.id ?? 'no parent panel';
}

The decorator equivalent reads identically through the constructor:

import { Component, Optional, SkipSelf, Host } from '@angular/core';
import { PanelService } from './panel.service';

@Component({ selector: 'app-child', standalone: true, template: '' })
export class ChildComponent {
  constructor(
    @Optional() @SkipSelf() @Host() private parent: PanelService | null,
  ) {}
}

Output:

<!-- when nested inside a PanelComponent -->
<span>a1b2c3</span>
<!-- when used standalone with no PanelComponent ancestor -->
<span>no parent panel</span>

When each modifier matters

  • @Self() is ideal for directives that require a sibling provider on the same element (for example a form control directive that must find its own NgControl).
  • @SkipSelf() lets a service delegate to a parent of the same type — handy for building chains or accumulating state up the tree.
  • @Host() is the cornerstone of component-author boundaries: it prevents a child directive from reaching past its host component into the wider app.
  • @Optional() keeps reusable components from crashing when an enhancement service simply isn’t provided.

Warning: @Host() does not mean “the root element.” It means “stop at the host of the component that declared this injection.” Misreading it is a frequent source of unexpected NullInjectorErrors in shared component libraries.

Best practices

  • Prefer providedIn: 'root' for stateless, app-wide singletons; reserve element-level providers for genuinely per-instance state.
  • Use viewProviders instead of providers when a helper service must stay internal to a component’s template and out of reach of projected content.
  • Reach for { optional: true } (or @Optional()) on any dependency that is an optional enhancement, so reusable components degrade gracefully.
  • Scope route-specific services with a route’s providers array rather than the root injector to keep lazy-loaded features isolated.
  • Combine skipSelf and host deliberately when authoring directives so they cannot accidentally resolve services from outside their host component.
  • Favor the inject() function with its options object in modern Angular — it is more composable than stacking constructor decorators and works in factory functions.
Last updated June 14, 2026
Was this helpful?