Angular Dependency Injection Advanced: Providers, Hierarchical DI & Resolution Modifiers
Master Angular's advanced DI patterns — providers, hierarchical injection, resolution modifiers, InjectionTokens, and real-world architecture patterns with troubleshooting guide.
- 1. Angular Dependency Injection Fundamentals: Services, inject() & Injection Context
- 2. Angular Dependency Injection Advanced: Providers, Hierarchical DI & Resolution Modifiers
Introduction
Welcome to Part 2 of the Angular Dependency Injection series! In Part 1: Fundamentals, we covered services, the inject() function, and injection context. Now it's time to unlock the advanced power of Angular's DI system.
📌 Quick Reference
Reading Time: ~13 minutes
Skill Level: Intermediate → Advanced
Prerequisites: Part 1: DI Fundamentals
Navigate by Section (with estimated time):
- Providers (6 min) - Manual configuration options
- Hierarchical DI (4 min) - Two injector hierarchies
- Resolution Modifiers (5 min) - @Optional, @Self, @SkipSelf, @Host
- Component Providers (3 min) - providers vs viewProviders
- InjectionToken (3 min) - Non-class dependencies
- Advanced Patterns (4 min) - forwardRef, multi providers
- Troubleshooting (2 min) - Decision tree for errors
- Cheat Sheet (1 min) - Quick reference card
🔙 Need fundamentals first? Start with Part 1: Services, inject() & Injection Context.
Understanding Providers
⏱️ ~6 minutes
In Part 1, we used providedIn: 'root' to make services available everywhere — and that works great for most cases. But what happens when you need more control?
providedIn: 'root' as a global cafeteria — everyone gets the same food from the same kitchen. But sometimes you need a private chef (component-scoped instance), a custom recipe (factory-based creation), or a menu card (configuration object). That's where providers come in — they let you customize what Angular delivers and how it creates it.What Exactly is a Provider?
A provider is an instruction that tells Angular's DI system: "When someone asks for X, give them Y."
Without providers, Angular only knows how to create simple class instances. Providers unlock the ability to:
- Swap one class for another (like using a mock in tests)
- Supply plain values like configuration objects
- Run custom logic to decide what to create
- Create aliases so old code works with new services
When to Use Manual Providers
You should manually configure providers when:
- 🔧 The service doesn't have providedIn – Third-party or library services without automatic provision must be manually provided
- 🔧 You want a new instance – Create a separate instance at the component level instead of using the shared singleton (e.g., each form editor needs its own state)
- 🔧 You need runtime configuration – Service behavior depends on runtime values like environment settings or user preferences
- 🔧 You're providing non-class values – Configuration objects, feature flags, API URLs, or primitive values that aren't classes
Provider Configuration Object
Every provider is like a recipe card with two parts:
- Token (the
provideproperty) – The unique key that consumers use to request the dependency. Think of it as the name on the order ticket. - Recipe – How to create the value. Angular supports four recipes:
useClass– "Create a new instance of this class" — provides a JavaScript classuseValue– "Use this exact value as-is" — provides a static valueuseFactory– "Run this function to create the value" — provides a factory function that returns the valueuseExisting– "Point to another provider" — creates an alias to an existing provider
Provider Types with Examples
1. Class Providers (useClass)
useClass tells Angular: "When someone asks for Service A, actually give them an instance of Service B." This is the strategy pattern built into Angular's DI — you can swap implementations without changing any consumer code.
When to use:
- ✅ Swapping implementations (testing, A/B testing)
- ✅ Providing a subclass instead of base class
- ✅ Using different implementations per environment
Real example: Use MockAuthService in tests, RealAuthService in production. The component code says inject(AuthService) in both cases — it doesn't know or care which implementation it gets.
Don't use if: You just need a simple class → use providedIn: 'root' instead
import { Component } from '@angular/core';
import { Logger } from './logger.service';
import { BetterLogger } from './better-logger.service';
@Component({
selector: 'app-hero',
providers: [
{ provide: Logger, useClass: BetterLogger }
]
})
export class HeroComponent {
// This component will get BetterLogger when it asks for Logger
}
2. Value Providers (useValue)
useValue tells Angular: "When someone asks for this token, give them this exact value." No class instantiation, no factory logic — just a direct handoff. This is perfect for configuration data that doesn't change at runtime.
When to use:
- ✅ Providing configuration objects (API URLs, timeouts, feature flags)
- ✅ Providing primitive values (strings, numbers, booleans)
- ✅ Providing pre-existing object instances that are already created
Real example: Your app needs to know the API base URL. Instead of hardcoding it everywhere, inject it as a value so you can change it in one place.
Don't use if: The value needs to be computed based on runtime conditions → use useFactory instead
const API_CONFIG = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
@Component({
selector: 'app-root',
providers: [
{ provide: 'API_CONFIG', useValue: API_CONFIG }
]
})
export class AppComponent {}
3. Factory Providers (useFactory)
useFactory tells Angular: "Run this function and use whatever it returns." This is the most flexible option — the factory function can contain any logic, including conditionals, calculations, or even injecting other dependencies to decide what to create.
When to use:
- ✅ Creating services based on runtime conditions (e.g., production vs development)
- ✅ Injecting other dependencies into the factory to configure the service
- ✅ Complex initialization logic that goes beyond a simple constructor
Real example: Use ProductionLogger (sends to external service) in production but DevelopmentLogger (console.log) during development — decided at bootstrap time.
Don't use if: A static value or simple class works → keep it simple
function loggerFactory() {
return environment.production
? new ProductionLogger()
: new DevelopmentLogger();
}
@Component({
selector: 'app-root',
providers: [
{ provide: Logger, useFactory: loggerFactory }
]
})
export class AppComponent {}
4. Alias Providers (useExisting)
useExisting tells Angular: "When someone asks for Token A, give them the same instance that Token B provides." Unlike useClass (which creates a new instance), useExisting points to an already-existing provider — both tokens share the exact same instance.
When to use:
- ✅ Creating aliases for renamed services (old name still works)
- ✅ Backward compatibility during refactoring (old code → new service, same instance)
- ✅ Multiple interfaces pointing to the same concrete implementation
Real example: You renamed OldLogger to NewLogger. Instead of updating 50 files at once, alias the old name to the new one. Both resolve to the same NewLogger instance.
Don't use if: You need separate, independent instances → use useClass instead
@Component({
providers: [
NewLogger,
{ provide: OldLogger, useExisting: NewLogger }
]
})
export class AppComponent {}
Hierarchical Dependency Injection
One of Angular's most powerful features is its hierarchical injector system. This is what makes Angular's DI far more powerful than a simple service container — it gives you scoped control over which parts of your app see which services.
Why Does This Matter?
Hierarchical DI lets you:
- 🎯 Scope services — Give a feature module its own instance of a service
- 🔒 Isolate state — Each form or editor can have its own private state service
- 🌳 Override behavior — A child component can provide a different implementation than the parent
- ⚡ Lazy load — Route-specific services are only created when that route is visited
Two Injector Hierarchies
Angular has two parallel injector trees that work together. Understanding both is key to knowing where your service comes from.
1. EnvironmentInjector Hierarchy (the "global" side)
This hierarchy manages application-wide services. Think of it as the corporate headquarters.
- Platform Injector – Shared across all Angular applications on the page (rare, only relevant in micro-frontend setups)
- Root Environment Injector – Your main application injector. This is where
providedIn: 'root'services live. - Route Injectors – Created for lazy-loaded routes. Services provided here only exist when that route is active, keeping your initial bundle small.
Services provided with providedIn: 'root' or in ApplicationConfig providers live here.
2. ElementInjector Hierarchy (the "component" side)
This hierarchy mirrors your component tree in the DOM. Think of it as the org chart — every component has its own injector.
- Created automatically for each component/directive in the component tree
- Mirrors the DOM structure of your application
- Services provided in component
providersarrays live here - Each component's injector can provide its own instances, overriding whatever the parent has
How Dependencies Are Resolved
When Angular needs to resolve a dependency, it follows this process:
Resolution Steps:
- Angular first looks in the ElementInjector hierarchy (component tree)
- It searches up the component tree until it finds a provider
- If not found, it searches the EnvironmentInjector hierarchy
- If still not found, Angular throws an error
Important: The first provider Angular finds is the one it uses, even if there are other providers higher up the tree.
Resolution Modifiers
Angular provides special modifiers to control how dependencies are resolved. These are essential for building robust libraries or dealing with optional dependencies.
1. @Optional() or { optional: true }
Scenario: You have a feature that might not be enabled, like a premium badge.
🛑 caution[!CAUTION] The Problem: Application Crash Trying to inject a service that hasn't been provided will cause your application to crash with a "No provider" error.
@Component({ ... })
export class UserCardComponent {
// 💥 CRASH: If PremiumBadgeService isn't provided, the app dies here.
private badgeService = inject(PremiumBadgeService);
hasPremiumBadge() {
return this.badgeService.isPremium();
}
}
💡 tip[!TIP] The Solution: Graceful Fallback Use
@Optional()to tell Angular it's okay if the dependency is missing.
export class UserCardComponent {
// ✅ SAFE: Returns null if service is missing
private badgeService = inject(PremiumBadgeService, { optional: true });
hasPremiumBadge() {
// We must check if it exists before using it
return this.badgeService?.isPremium() ?? false;
}
}
2. @Self() or { self: true }
Scenario: A reusable component that must have its own local state.
🛑 caution[!CAUTION] The Problem: Accidental Shared State If you forget to provide a service in a component that is meant to have its own local instance, it might silently find and share a parent's instance. This leads to weird bugs where two independent components update the same data.
@Component({
selector: 'app-file-uploader',
// 😱 OOPS: We forgot 'providers: [UploadStateService]'
template: `...`
})
export class FileUploaderComponent {
// We THINK we have our own state, but we're actually
// overwriting the state of a parent or sibling uploader!
private state = inject(UploadStateService);
}
💡 tip[!TIP] The Solution: Enforce Local Instance Use
@Self()to force a local lookup. If you forgot to provide it, Angular will throw an error to alert you immediately.
@Component({
selector: 'app-file-uploader',
providers: [UploadStateService] // 👈 @Self() forces us to have this!
})
export class FileUploaderComponent {
// ✅ SAFE: Guarantees this instance belongs ONLY to this component
private state = inject(UploadStateService, { self: true });
}
3. @SkipSelf() or { skipSelf: true }
Scenario: A component that needs to talk to a "parent" version of a service.
🛑 caution[!CAUTION] The Problem: Shadowing Prevents Access Sometimes a component provides a service to configure its children, but it also needs to communicate with the global version of that same service. By default, it sees its own local instance.
@Component({
selector: 'app-toast-alert',
providers: [
// We provide a local generic config for children...
{ provide: ToastConfig, useValue: { position: 'bottom' } }
]
})
export class ToastAlertComponent {
// ...but we want to register with the GLOBAL tracker!
// ❌ FAIL: This injects the local instance we might have implicitly provided or shadowed!
private globalTracker = inject(ToastTrackerService);
}
💡 tip[!TIP] The Solution: Bypass Local Provider Use
@SkipSelf()to ignore the provider on the current component (the "Self") and start searching from the parent.
export class ToastAlertComponent {
// ✅ SUCCESS: Skips any local provider and finds the parent (global) tracker
private globalTracker = inject(ToastTrackerService, { skipSelf: true });
constructor() {
this.globalTracker.register(this);
}
}
4. @Host() or { host: true }
Scenario: A directive communicating with its host component.
🛑 caution[!CAUTION] The Problem: Over-reaching Search Directives usually want to interact with the component they are attached to. Without limits, they might find a matching component generic component far up the DOM tree (like a grandparent).
@Directive({ selector: '[appTooltip]' })
export class TooltipDirective {
// We want the button this directive is on.
// ❌ RISK: If this isn't on a button, it might keep searching up
// and find a 'ButtonComponent' wrapping the whole page!
private hostButton = inject(ButtonComponent);
}
💡 tip[!TIP] The Solution: Restrict to Host Use
@Host()to tell Angular: "Only look at the element I'm attached to."
export class TooltipDirective {
// ✅ SAFE: Only looks at the element this directive is attached to.
// If the host isn't a ButtonComponent, it stops there (and errors or returns null).
private hostButton = inject(ButtonComponent, {
host: true,
optional: true
});
}
Combining Modifiers
You can mix and match these modifiers for powerful patterns. A common pattern in recursive components (like a tree menu) is to use @Optional() and @SkipSelf() to find a parent of the same type.
export class TreeItemComponent {
// Find a parent TreeItemComponent, if one exists (recursion!)
// If I am the root item, this will be null.
private parentItem = inject(TreeItemComponent, {
optional: true,
skipSelf: true
});
}
Component-Level Providers
So far, we've mostly seen providedIn: 'root' (global singleton). But one of Angular's killer features is the ability to provide services at the component level — giving each component its own private instance.
providedIn: 'root' as a shared whiteboard in the office — everyone reads and writes to the same board. Component-level providers are like giving each team their own private whiteboard. Changes on one don't affect the others.providers vs viewProviders
Angular components have two provider arrays, and the difference matters when you use content projection (<ng-content>).
providers Array
The most common option. Makes services available to:
- ✅ The component itself
- ✅ All child components in the template
- ✅ Content projected into the component (via
<ng-content>)
In other words, providers shares with everyone — both your template children and any content projected from outside.
@Component({
selector: 'app-parent',
providers: [SharedService],
template: `
<app-child></app-child> <!-- ✅ Can access SharedService -->
<ng-content></ng-content> <!-- ✅ Projected content can too -->
`
})
export class ParentComponent {}
viewProviders Array
A more restrictive option. Makes services available to:
- ✅ The component itself
- ✅ Child components in the template (the "view")
- ❌ NOT available to projected content
Why would you want this? Encapsulation. If you're building a reusable library component (like a datepicker), you might have internal services that projected content from the consumer should not be able to access or override.
@Component({
selector: 'app-parent',
viewProviders: [ViewOnlyService],
template: `
<app-child></app-child> <!-- ✅ Can access ViewOnlyService -->
<ng-content></ng-content> <!-- ❌ Cannot access ViewOnlyService -->
`
})
export class ParentComponent {}
💡 tip[!TIP] When to use which?
- Use
providers(default) unless you have a specific reason to hide services from projected content.- Use
viewProviderswhen building library or reusable components where you want to keep internal services private.
Use Cases for Component Providers
Scenario 1: Service Isolation
Imagine you have two file uploaders on the same page. Without component providers, they'd share the same UploadStateService (root singleton) — uploading in one would affect the other! Component providers fix this:
@Component({
selector: 'app-hero-editor',
providers: [HeroService] // Each editor gets its OWN HeroService instance
})
export class HeroEditorComponent {}
Now, every <app-hero-editor> on the page has a completely independent HeroService.
Scenario 2: Multiple Edit Sessions
A common real-world pattern — tabs or dialogs where each one manages its own form state:
@Component({
selector: 'app-form-editor',
providers: [FormStateService] // Each form tab has its own state
})
export class FormEditorComponent {
private formState = inject(FormStateService);
// This instance is independent from other form editors.
// Closing one tab won't lose data in another!
}
InjectionToken for Non-Class Dependencies
So far, every dependency we've injected has been a class — UserService, HttpClient, Router. But what if you need to inject a configuration object, a string, or a boolean flag? You can't write inject(string) — TypeScript won't know which string you mean!
That's the problem InjectionToken solves.
Why Not Just Use Strings as Tokens?
You might think: "Can't I just use { provide: 'API_URL', useValue: 'https://...' }?"
Technically yes, but it has serious downsides:
- ❌ No type safety —
inject('API_URL')returnsany, so you lose TypeScript's protection - ❌ Name collisions — Two libraries could both use the string
'API_URL'and overwrite each other - ❌ No tree-shaking — Angular can't optimize away unused string-keyed providers
InjectionToken fixes all of this by creating a unique, typed, collision-proof token.
Creating an InjectionToken
import { InjectionToken } from '@angular/core';
// Step 1: Define the shape of your config
export interface AppConfig {
apiUrl: string;
production: boolean;
}
// Step 2: Create a unique token with a type parameter
// The string 'app.config' is just a description for debugging — it's not the key!
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
Providing the Token
const myConfig: AppConfig = {
apiUrl: 'https://api.example.com',
production: true
};
@Component({
providers: [
// "When someone asks for APP_CONFIG, give them myConfig"
{ provide: APP_CONFIG, useValue: myConfig }
]
})
export class AppComponent {}
Injecting the Token
import { inject } from '@angular/core';
export class MyService {
// TypeScript knows this is AppConfig — full autocomplete!
private config = inject(APP_CONFIG);
getApiUrl() {
return this.config.apiUrl; // ✅ Type-safe access
}
}
InjectionToken with providedIn (Self-Providing Tokens)
Just like services, tokens can provide their own default value using a factory function. This means consumers don't need to explicitly provide the token — it works out of the box with sensible defaults:
export const API_URL = new InjectionToken<string>('api.url', {
providedIn: 'root',
factory: () => 'https://api.example.com' // Default value
});
// Now anyone can inject(API_URL) without additional setup!
// And you can still override it at the component/route level if needed.
💡 tip[!TIP] When to use
InjectionToken:
- Configuration objects (API URLs, feature flags, theme settings)
- Constants that multiple services need (app version, environment name)
- Abstract interfaces where you want to swap implementations via DI
- Any non-class value you want to inject with type safety
Advanced DI Patterns
These patterns go beyond the basics and are commonly used in large-scale Angular applications and libraries. Each pattern solves a specific architectural challenge.
Pattern 1: Forward References
The problem: In TypeScript, you can't reference a class before it's declared in the file. This becomes an issue when two entities need to reference each other, or when the declaration order doesn't match the usage order.
The solution: forwardRef wraps the reference in an arrow function, deferring the lookup until Angular actually needs it:
import { forwardRef } from '@angular/core';
@Component({
providers: [
{
// ChildService is declared BELOW this line in the file,
// so we need forwardRef to tell Angular "trust me, it'll exist"
provide: ParentService,
useExisting: forwardRef(() => ChildService)
}
]
})
export class MyComponent {}
When you'll encounter this: Mostly when building recursive component structures (tree views) or when TypeScript module ordering creates circular references.
Pattern 2: Multi Providers
The problem: Normally, each token has exactly one provider. But what if you want a plugin system where multiple modules can each contribute their own implementation?
The solution: The multi: true flag tells Angular to collect all providers with the same token into an array, instead of overwriting:
export const PLUGIN_TOKEN = new InjectionToken<Plugin[]>('plugins');
@NgModule({
providers: [
{ provide: PLUGIN_TOKEN, useClass: Plugin1, multi: true },
{ provide: PLUGIN_TOKEN, useClass: Plugin2, multi: true }
]
})
export class AppModule {}
When injected, you get an array of all registered providers:
private plugins = inject(PLUGIN_TOKEN); // [Plugin1Instance, Plugin2Instance]
Real-world uses: Angular itself uses multi providers extensively — HTTP_INTERCEPTORS is a multi-provider token that lets you register multiple interceptors. You've probably used this pattern without knowing it!
Pattern 3: Provide Functions (Library Pattern)
The problem: When building a library or feature module, consumers need to configure multiple related providers together. Expecting them to manually list 3-5 providers with the right tokens is error-prone.
The solution: Create a provideXxx() helper function that bundles everything together. This is the pattern Angular itself uses (provideRouter(), provideHttpClient(), etc.):
// Library code — encapsulates all configuration
export function provideAnalytics(config: AnalyticsConfig) {
return [
{ provide: ANALYTICS_CONFIG, useValue: config },
AnalyticsService,
AnalyticsLogger
];
}
// Consumer code — clean, one-liner setup
bootstrapApplication(AppComponent, {
providers: [
provideAnalytics({ trackingId: 'UA-12345' })
]
});
Why it's great: The consumer doesn't need to know about ANALYTICS_CONFIG, AnalyticsService, or AnalyticsLogger individually. They just call one function. If you add a new internal service later, existing consumers don't need to change anything.
Injection Context Best Practices
When You're in an Injection Context
The inject() function only works in certain places:
✅ Constructor of @Injectable or @Component classes
✅ Field initializers in these classes
✅ Factory functions in providers
✅ InjectionToken factory functions
Using inject() Outside a Context
If you need to use DI outside these contexts, use runInInjectionContext():
import { runInInjectionContext, inject } from '@angular/core';
class MyClass {
constructor(private injector: Injector) {}
doSomething() {
runInInjectionContext(this.injector, () => {
const service = inject(MyService);
service.doWork();
});
}
}
ℹ️ note[!NOTE] For a thorough explanation of injection context, see the CIFF vs LATE rule in Part 1.
Real-World Example: Complete Feature
// 1. Define the service
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private notifications$ = new BehaviorSubject<string[]>([]);
addNotification(message: string) {
const current = this.notifications$.value;
this.notifications$.next([...current, message]);
}
getNotifications() {
return this.notifications$.asObservable();
}
}
// 2. Create a configuration token
export const NOTIFICATION_CONFIG = new InjectionToken<NotificationConfig>(
'notification.config',
{
providedIn: 'root',
factory: () => ({ duration: 3000, position: 'top-right' })
}
);
// 3. Use in a component
@Component({
selector: 'app-notifications',
template: `
<div class="notification" *ngFor="let msg of notifications$ | async">
{{ msg }}
</div>
`
})
export class NotificationsComponent {
private notificationService = inject(NotificationService);
private config = inject(NOTIFICATION_CONFIG);
notifications$ = this.notificationService.getNotifications();
ngOnInit() {
console.log('Notification duration:', this.config.duration);
}
}
Common Pitfalls and How to Avoid Them
❌ Pitfall 1: Using inject() Outside Injection Context
Wrong:
export class MyComponent {
ngOnInit() {
const service = inject(MyService); // ERROR!
}
}
Correct:
export class MyComponent {
private service = inject(MyService); // ✅ Field initializer
ngOnInit() {
this.service.doSomething();
}
}
❌ Pitfall 2: Forgetting @Injectable Decorator
Wrong:
export class MyService { // Missing decorator!
doSomething() {}
}
Correct:
@Injectable({
providedIn: 'root'
})
export class MyService {
doSomething() {}
}
❌ Pitfall 3: Circular Dependencies
Problem: ServiceA depends on ServiceB, and ServiceB depends on ServiceA
Solution: Restructure your code or use forwardRef() if absolutely necessary
Performance Tips
Angular's DI system is designed to be fast, but how you configure providers can significantly impact your app's bundle size and memory usage. Here are three strategies that matter most.
1. Use providedIn: 'root' for Singletons
Why it matters: When you use providedIn: 'root', Angular's build system can tree-shake the service — if no component actually injects it, the service code is completely excluded from the final bundle. This doesn't happen with manual providers in @NgModule or component providers arrays.
@Injectable({
providedIn: 'root' // ✅ Tree-shakeable. If unused, it's removed from the bundle.
})
export class MyService {}
Fun fact: If you provide a service in an @NgModule providers array, Angular includes it in the bundle even if nothing in the app imports it. With providedIn: 'root', it only shows up if someone actually calls inject(MyService).
2. Provide Services Only Where Needed
Why it matters: Every service provided at the root level stays in memory for the entire app lifecycle. If a service is only used inside one feature, provide it at the component level — Angular will create the instance only when that component appears and destroy it when the component is removed from the DOM.
@Component({
providers: [LocalOnlyService] // Created on render, destroyed on removal
})
export class FeatureComponent {}
This also prevents memory leaks from services that hold state (subscriptions, caches) but are only relevant to a specific feature.
3. Lazy Load with Route Providers
Why it matters: Services provided at the route level are code-split along with the route. The JavaScript for the service isn't even downloaded until the user navigates to that route. This can dramatically reduce your initial load time for services only needed in admin panels, settings pages, etc.
const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin.component'),
providers: [AdminService] // Only downloaded + created when user visits /admin
}
];
The impact: If AdminService pulls in heavy libraries (chart rendering, PDF generation, etc.), none of that code is loaded until the user actually visits the admin route.
🔧 Troubleshooting Guide
Your DI error-solving toolkit
Got an error? Follow this decision tree to solve common problems:
❌ Error: "inject() must be called from an injection context"
What it means: You're calling inject() outside the initialization phase (during LATE, not CIFF).
Quick fix:
// ❌ Wrong - in lifecycle hook
ngOnInit() {
const service = inject(MyService); // ERROR!
}
// ✅ Correct - in field initializer
private service = inject(MyService); // Works!
Remember: Inject during CIFF, use during LATE! (See Part 1 for the full explanation.)
❌ Error: "No provider for XyzService"
What it means: Angular can't find your service.
Checklist:
- ✅ Add
@Injectable()decorator to the service - ✅ Set
providedIn: 'root'in the decorator - ✅ Check for typos in import path
- ✅ Verify service is in
providersarray (if not usingprovidedIn)
Quick fix:
@Injectable({ providedIn: 'root' }) // Add this!
export class MyService {}
❌ Error: "Circular dependency detected"
What it means: ServiceA → ServiceB → ServiceA
Solutions (in order):
- Best: Restructure - create a third service
- OK: Use
forwardRef(() => ServiceName) - Last resort: Inject lazily when needed
🔄 Decision Tree
Got DI error?
│
├─ "inject() must be called from injection context"
│ └─ ✅ Move inject() to field initializer
│
├─ "No provider for X"
│ ├─ Missing @Injectable()? → ✅ Add it
│ ├─ Missing providedIn? → ✅ Add 'root'
│ └─ Typo in import? → ✅ Check path
│
├─ "Circular dependency"
│ └─ ✅ Use forwardRef() or restructure
│
└─ Service not a singleton?
└─ ✅ Remove from component providers
← Scroll to view full diagram →
Conclusion
Dependency Injection is the backbone of Angular applications. By mastering DI, you can:
✅ Write cleaner, more maintainable code
✅ Build scalable applications with reusable services
✅ Create testable components and services
✅ Control service lifecycles and scopes precisely
✅ Optimize your application's bundle size
Key Takeaways
- Use
providedIn: 'root'for most services—it's simple and enables tree-shaking - Inject with
inject()in field initializers—it's clean and modern - Understand the injector hierarchy to control where services come from
- Use resolution modifiers (
optional,self,skipSelf,host) when you need precise control - Leverage InjectionTokens for non-class dependencies like configuration objects
- Provide at the right level—component, route, or root depending on your needs
Going Further
Now that you understand Angular's DI system, you can:
- Build complex applications with confidence
- Design better service architectures
- Optimize your application's performance
- Write more effective unit tests
- Tackle advanced Angular patterns
Remember, Dependency Injection might seem complex at first, but with practice, it becomes second nature. Start simple with providedIn: 'root' and inject(), then gradually explore more advanced patterns as your needs grow!
🔙 Missed the fundamentals? Read Part 1: Services, inject() & Injection Context
📄 DI Cheat Sheet
Print this for quick reference!
Creating Services
@Injectable({ providedIn: 'root' }) // ✅ Most common
export class MyService { }
Injecting Services
// ✅ Modern (recommended)
private service = inject(MyService);
// ⚠️ Old way (still works)
constructor(private service: MyService) {}
Provider Types
// Class
{ provide: Logger, useClass: BetterLogger }
// Value
{ provide: 'API_URL', useValue: 'https://api.com' }
// Factory
{ provide: Logger, useFactory: () => new Logger() }
// Alias
{ provide: OldService, useExisting: NewService }
Injection Modifiers
private service = inject(MyService, { optional: true }); // Don't error if missing
private service = inject(MyService, { self: true }); // Only this component
private service = inject(MyService, { skipSelf: true }); // Skip this component
private service = inject(MyService, { host: true }); // Stop at host
Memory Aid: CIFF vs LATE
inject() works during CIFF:
- Constructor
- Initializers (field)
- Factory functions
- First-time setup
inject() fails during LATE:
- Lifecycle hooks
- Async callbacks
- Timers
- Event handlers
Common Errors & Fixes
| Error | Fix |
|---|---|
| inject() context error | Move to field initializer |
| No provider | Add @Injectable({ providedIn: 'root' }) |
| Circular dependency | Use forwardRef() or restructure |
| Not singleton | Remove from component providers |
📌 Bookmark this page for quick reference!
Happy coding! 🚀