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.
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.
You now understand how Angular's dependency injection system works. Next, learn about Providers to see the different ways you can configure what DI actually supplies.