Skip to content

Angular Dependency Injection

Dependency injection (DI) is the mechanism Angular uses to supply classes with the instances they depend on, without those classes creating dependencies themselves. This lesson explains how Angular's DI system actually works.

What Is Dependency Injection?

Dependency injection is a design pattern where a class declares what it needs (its "dependencies") instead of constructing them itself, and an external system (the "injector") supplies matching instances at runtime.

In Angular, nearly everything, components, services, pipes, directives, guards, can request dependencies this way, and Angular's injector figures out how to create and supply them automatically.

@Injectable({ providedIn: 'root' })
export class LoggerService {
  log(message: string) { console.log(message); }
}

@Component({ selector: 'app-root', standalone: true, template: '' })
export class AppComponent {
  private logger = inject(LoggerService);

  ngOnInit() {
    this.logger.log('App started');
  }
}

AppComponent never creates a LoggerService instance itself, Angular's injector resolves and supplies it via inject().

The Injector Tree

Platform Injector
  └── Root Injector (providedIn: 'root')
        └── Route/Component Injector (providers: [...] on a component)
              └── Child Component Injector
  • Angular resolves a dependency by walking up the injector tree until a matching provider is found.
  • A provider registered closer to where it's requested (like on a component) takes priority over one registered higher up (like root).
  • Providing a service at the root injector gives one shared instance app-wide.
  • Providing the same service on a specific component gives that component's subtree its own separate instance.

Dependency Injection Cheatsheet

Where and how you can register and consume dependencies.

Location Example Scope
Root (service decorator) @Injectable({ providedIn: 'root' }) Whole app, one instance
Component providers providers: [FeatureService] That component and its children
Route providers providers: [...] on a route That route and its children
App config providers providers: [provideRouter(routes)] Whole app (bootstrap-level)
Injecting inject(MyService) Wherever DI context is available
Optional dependency inject(MyService, { optional: true }) Returns null if not provided

How Angular Resolves a Dependency

When a class asks for a dependency (via inject() or a constructor parameter), Angular looks for a matching provider starting from the injector closest to that class, and walks upward through the injector tree until it finds one, or throws a NullInjectorError if none exists anywhere in the chain.

  • Angular checks the local injector (like a component's own providers) first.
  • If not found, it checks the parent injector, and continues upward.
  • The root injector is checked last, providedIn: 'root' services live here.
  • If nothing provides the dependency anywhere in the chain, Angular throws an error at runtime.

Why Dependency Injection Matters

DI decouples a class from the concrete implementation of its dependencies, which makes unit testing dramatically easier (you can inject a fake/mock service in tests) and lets Angular manage object lifetimes (like singleton services) consistently across the whole app.

// Testing with a mock, no real HTTP calls needed
TestBed.configureTestingModule({
  providers: [{ provide: UserService, useValue: mockUserService }],
});

Injection Tokens for Non-Class Dependencies

Not every dependency is a class, sometimes you need to inject a plain configuration object or a primitive value. InjectionToken provides a unique, type-safe identifier for these cases.

export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');

// providing it
{ provide: API_BASE_URL, useValue: 'https://api.example.com' }

// consuming it
private baseUrl = inject(API_BASE_URL);

Common Mistakes

  • Assuming a service registered on one component is automatically available everywhere; scope depends on where it's provided in the injector tree.
  • Forgetting that providedIn: 'root' and also listing the same service in a component's providers array creates two separate instances, not one shared instance.
  • Trying to call inject() outside of a valid DI context (like inside a plain function called from setTimeout).
  • Using a plain object as a dependency identifier instead of a proper InjectionToken, causing type-safety issues.

Key Takeaways

  • Dependency injection supplies classes with the instances they need, rather than having them construct dependencies directly.
  • Angular resolves dependencies by walking up a tree of injectors, from local to root.
  • Where a service is provided determines its scope: one shared instance vs a separate instance per subtree.
  • InjectionToken lets you inject non-class values (like configuration) through the same DI system.

Pro Tip

When debugging "no provider found" errors, think about the injector tree: is the service actually provided somewhere your class can see, at the root, on a parent component, or on the relevant route? The fix is almost always a scoping issue.