If you have ever encountered the NG0203 error — inject() must be called from an injection context — and spent time wondering why a perfectly valid service injection works in one place but fails in another, this article explains exactly why.
Angular’s dependency injection does not work everywhere. It requires a specific runtime environment called the Injection Context. Once you understand what that is, where it exists, and how to create it manually, you will stop guessing and start writing predictable, testable Angular code.
This article covers:
- What Injection Context is and why it matters
- Every location where it exists in an Angular application
- How to use
runInInjectionContextandassertInInjectionContext - Modern DI patterns with signals and standalone components
- Unit testing code that depends on injection context
Before we dive into the examples, a quick note: The code snippets provided here may be syntax from earlier Angular versions. Code Snippets just for understanding only.
What Is Dependency Injection Context?
Injection Context is the execution environment where Angular’s DI system is active. It is the runtime state in which an injector is available, the inject() function can resolve tokens, and Angular understands the component or service hierarchy you are operating within.
Without it, inject() has no injector to consult and throws NG0203.
// Works — field initializer inside a component class
@Component({
selector: 'app-tyuser',
template: `...`,
standalone: true
})
export class UserComponent {
private userService = inject(UserService); // Injection context is active here
}
// Fails - standalone function with no injector
function randomFunction() {
const service = inject(SomeService); // Error NG0203
}
Where Injection Context Exists
Injection context is not magic — it exists in specific, predictable places.
1. Class Constructors
Any class instantiated by Angular’s DI system has injection context in its constructor. Both inject() and constructor parameter injection work here.
@Injectable({ providedIn: 'root' })
export class DataService {
private http = inject(HttpClient);
constructor() {
// Injection context is active inside constructors of DI-managed classes
const logger = inject(LoggerService);
}
}
@Component({
selector: 'app-dashboard',
template: `
@if (data()) {
<div>{{ data() }}</div>
}
`,
standalone: true
})
export class DashboardComponent {
private dataService = inject(DataService);
constructor() {
// Also valid here
}
}
2. Field Initializers
Field initializers on Angular-managed classes run during construction, so injection context is active. This is the modern, preferred style — no constructor boilerplate required.
@Component({
selector: 'app-profile',
template: `
@for (user of users(); track user.id) {
<user-card [user]="user" />
}
`,
standalone: true,
imports: [UserCardComponent]
})
export class ProfileComponent {
private userService = inject(UserService);
private destroyRef = inject(DestroyRef);
users = signal<User[]>([]);
ngOnInit() {
this.userService.getUsers()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(users => this.users.set(users));
}
}
3. Factory Functions in Providers
Factory functions used in provider definitions — including InjectionToken factories — run inside injection context. You can call inject() freely within them.
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG', {
providedIn: 'root',
factory: () => {
// Injection context is active inside factory functions
const env = inject(EnvironmentService);
return {
baseUrl: env.apiUrl,
timeout: 5000
};
}
});
@Component({
selector: 'app-api-consumer',
template: `...`,
standalone: true
})
export class ApiConsumerComponent {
private config = inject(API_CONFIG);
}
4. Functional Guards and Resolvers
Angular’s functional guard and resolver APIs (CanActivateFn, ResolveFn, etc.) run inside injection context, so you can call inject() directly inside them.
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.component'),
canActivate: [authGuard]
}
];
A resolver follows the same pattern:
export const userResolver: ResolveFn<User> = (route) => {
const userService = inject(UserService);
const userId = route.params['id'];
return userService.getUser(userId);
};
@Component({
selector: 'app-user-detail',
template: `
@if (user()) {
<h1>{{ user()!.name }}</h1>
}
`,
standalone: true
})
export class UserDetailComponent {
private route = inject(ActivatedRoute);
user = toSignal(
this.route.data.pipe(map(data => data['user'] as User))
);
}
Running Code in Injection Context: runInInjectionContext
Sometimes you need to call inject() from a place where injection context does not naturally exist — for example, inside a utility function called from a component method. runInInjectionContext solves this by manually establishing the context.
The Problem
// This fails — no injection context in a standalone utility function
export function logUserAction(action: string) {
const logger = inject(LoggerService); // Error NG0203
logger.log(`User action: ${action}`);
}
The Solution
@Component({
selector: 'app-activity',
template: `
<button (click)="trackAction('clicked')">Track</button>
`,
standalone: true
})
export class ActivityComponent {
private injector = inject(EnvironmentInjector);
trackAction(action: string) {
runInInjectionContext(this.injector, () => {
logUserAction(action); // Now injection context is active
});
}
}
export function logUserAction(action: string) {
const logger = inject(LoggerService);
const userService = inject(UserService);
logger.log({
action,
userId: userService.currentUser?.id,
timestamp: Date.now()
});
}
The key is injecting EnvironmentInjector (not Injector) when you need to pass it to runInInjectionContext, since that function requires an EnvironmentInjector or Injector instance.
Real-World Use: Service Method with Dynamic Injection
@Injectable({ providedIn: 'root' })
export class ApiService {
private injector = inject(EnvironmentInjector);
private http = inject(HttpClient);
makeAuthenticatedRequest<T>(url: string): Observable<T> {
return runInInjectionContext(this.injector, () => {
const auth = inject(AuthService);
const token = auth.getToken();
return this.http.get<T>(url, {
headers: { Authorization: `Bearer ${token}` }
});
});
}
}
Note: in most cases it is cleaner to inject AuthService as a field. runInInjectionContext is best reserved for utilities and helpers that must remain decoupled from a specific class.
Writing Defensive Code: assertInInjectionContext
If you are writing a utility function that relies on injection context, you should call assertInInjectionContext at the top. This produces a clear, informative error immediately if the function is called in the wrong place, rather than a cryptic failure later.
export function createSignalFromObservable<T>(
observableFactory: () => Observable<T>
): Signal<T | undefined> {
// Fail immediately with a helpful message if called outside injection context
assertInInjectionContext(createSignalFromObservable);
const result = signal<T | undefined>(undefined);
const destroyRef = inject(DestroyRef);
observableFactory()
.pipe(takeUntilDestroyed(destroyRef))
.subscribe(value => result.set(value));
return result.asReadonly();
}
// This works - called from a field initializer (injection context is active)
@Component({
selector: 'app-data-viewer',
template: `
@if (userData()) {
<pre>{{ userData() | json }}</pre>
}
`,
standalone: true,
imports: [JsonPipe]
})
export class DataViewerComponent {
private userService = inject(UserService);
userData = createSignalFromObservable(() => this.userService.getCurrentUser());
}
Debugging Injection Context
This helper can be useful during development to verify whether a code path is running inside injection context:
export function debugInjectionContext(label: string): boolean {
try {
assertInInjectionContext(debugInjectionContext);
console.log(`[OK] ${label}: inside injection context`);
return true;
} catch {
console.error(`[FAIL] ${label}: outside injection context`);
return false;
}
}
@Component({
selector: 'app-debug',
template: `...`,
standalone: true
})
export class DebugComponent {
constructor() {
debugInjectionContext('Constructor'); // Logs: [OK]
}
ngOnInit() {
debugInjectionContext('ngOnInit'); // Logs: [FAIL]
}
someMethod() {
debugInjectionContext('someMethod'); // Logs: [FAIL]
}
}
ngOnInit and other lifecycle hooks run outside injection context — only constructors and field initializers are safe for inject() without runInInjectionContext.
Signals and Injection Context
Angular signals integrate naturally with the injection system. Here are two common patterns.
Reactive Service with a Signal
@Injectable({ providedIn: 'root' })
export class ThemeService {
private storage = inject(LocalStorageService);
private themeSignal = signal<'light' | 'dark'>(
(this.storage.getItem('theme') as 'light' | 'dark') ?? 'light'
);
readonly theme = this.themeSignal.asReadonly();
toggleTheme(): void {
const next = this.themeSignal() === 'light' ? 'dark' : 'light';
this.themeSignal.set(next);
this.storage.setItem('theme', next);
}
}
@Component({
selector: 'app-theme-toggle',
template: `
<button (click)="themeService.toggleTheme()">
@if (themeService.theme() === 'dark') {
Switch to Light
} @else {
Switch to Dark
}
</button>
`,
standalone: true
})
export class ThemeToggleComponent {
protected themeService = inject(ThemeService);
}
Computed Signal from Multiple Injected Services
@Component({
selector: 'app-user-stats',
template: `
<div class="stats-grid">
@for (stat of userStats(); track stat.label) {
<div class="stat-card">
<h3>{{ stat.label }}</h3>
<p>{{ stat.value }}</p>
</div>
}
</div>
`,
standalone: true
})
export class UserStatsComponent {
private userService = inject(UserService);
private analyticsService = inject(AnalyticsService);
private user = toSignal(this.userService.currentUser$);
userStats = computed(() => {
const currentUser = this.user();
if (!currentUser) return [];
return [
{ label: 'Posts', value: currentUser.postCount },
{ label: 'Followers', value: currentUser.followerCount },
{
label: 'Engagement',
value: this.analyticsService.calculateEngagement(currentUser)
}
];
});
}
Unit Testing Code That Uses Injection Context
Testing a Component with inject()
describe('UserProfileComponent', () => {
let component: UserProfileComponent;
let fixture: ComponentFixture<UserProfileComponent>;
let userService: jasmine.SpyObj<UserService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('UserService', ['getUser', 'updateUser']);
TestBed.configureTestingModule({
imports: [UserProfileComponent],
providers: [{ provide: UserService, useValue: spy }]
});
fixture = TestBed.createComponent(UserProfileComponent);
component = fixture.componentInstance;
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});
it('should inject UserService', () => {
expect(component['userService']).toBe(userService);
});
it('should load user data on init', () => {
const mockUser = { id: 1, name: 'Test User' };
userService.getUser.and.returnValue(of(mockUser));
component.ngOnInit();
expect(userService.getUser).toHaveBeenCalled();
expect(component.user()).toEqual(mockUser);
});
});
Testing a Utility That Calls inject()
Use TestBed.runInInjectionContext to execute functions that require injection context:
describe('logUserAction', () => {
let injector: EnvironmentInjector;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [LoggerService, UserService]
});
injector = TestBed.inject(EnvironmentInjector);
});
it('should log the action when called inside injection context', () => {
const loggerSpy = spyOn(TestBed.inject(LoggerService), 'log');
TestBed.runInInjectionContext(() => {
logUserAction('test-action');
});
expect(loggerSpy).toHaveBeenCalledWith(
jasmine.objectContaining({ action: 'test-action' })
);
});
it('should throw NG0203 when called outside injection context', () => {
expect(() => logUserAction('test')).toThrowError(/NG0203/);
});
});
Testing a Functional Guard
describe('authGuard', () => {
let authService: jasmine.SpyObj<AuthService>;
let router: jasmine.SpyObj<Router>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useValue: jasmine.createSpyObj('AuthService', ['isAuthenticated']) },
{ provide: Router, useValue: jasmine.createSpyObj('Router', ['createUrlTree']) }
]
});
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
});
it('should return true when the user is authenticated', () => {
authService.isAuthenticated.and.returnValue(true);
const result = TestBed.runInInjectionContext(() =>
authGuard({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot)
);
expect(result).toBe(true);
});
it('should redirect to /login when not authenticated', () => {
authService.isAuthenticated.and.returnValue(false);
const urlTree = {} as UrlTree;
router.createUrlTree.and.returnValue(urlTree);
const result = TestBed.runInInjectionContext(() =>
authGuard({} as ActivatedRouteSnapshot, { url: '/protected' } as RouterStateSnapshot)
);
expect(router.createUrlTree).toHaveBeenCalledWith(
['/login'],
{ queryParams: { returnUrl: '/protected' } }
);
expect(result).toBe(urlTree);
});
});
Best Practices
Prefer inject() over constructor parameter injection. The result is less boilerplate and clearer intent, especially when a class depends on many services.
// Constructor injection — verbose at scale
@Component({ selector: 'app-old', template: `...`, standalone: true })
export class OldStyleComponent {
constructor(
private userService: UserService,
private router: Router,
private http: HttpClient
) {}
}
// Field injection - concise and explicit
@Component({ selector: 'app-modern', template: `...`, standalone: true })
export class ModernComponent {
private userService = inject(UserService);
private router = inject(Router);
private http = inject(HttpClient);
}
Use DestroyRef for subscription cleanup. Inject DestroyRef and use takeUntilDestroyed instead of managing Subject-based teardown manually.
@Component({
selector: 'app-live-data',
template: `...`,
standalone: true
})
export class LiveDataComponent {
private destroyRef = inject(DestroyRef);
private dataService = inject(DataService);
data = signal<any[]>([]);
ngOnInit() {
this.dataService.getData()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => this.data.set(data));
}
}
Use InjectionToken with factory for platform-aware configuration.
export interface AppConfig {
apiUrl: string;
version: string;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
bootstrapApplication(AppComponent, {
providers: [
{
provide: APP_CONFIG,
useFactory: () => {
const platformId = inject(PLATFORM_ID);
return {
apiUrl: isPlatformBrowser(platformId) ? '/api' : 'http://localhost:3000',
version: '2.0.0'
};
}
}
]
});
Summary
Injection context is the runtime environment in which Angular’s injector is active and inject() can resolve dependencies. It exists in:
- Constructors of classes managed by Angular’s DI system
- Field initializers of those same classes
- Factory functions in provider and
InjectionTokendefinitions - Functional guards and resolvers (
CanActivateFn,ResolveFn, etc.)
When you need injection context outside these locations, use runInInjectionContext with an EnvironmentInjector. When writing utility functions that depend on context, call assertInInjectionContext at the top to produce a clear error on misuse. In tests, use TestBed.runInInjectionContext to test any function that calls inject() directly.
Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
- 💼 LinkedIn — Let’s connect professionally
- 🎥 Threads — Short-form frontend insights
- 🐦 X (Twitter) — Developer banter + code snippets
- 👥 BlueSky — Stay up to date on frontend trends
- 🌟 GitHub Projects — Explore code in action
- 🌐 Website — Everything in one place
- 📚 Medium Blog — Long-form content and deep-dives
- 💬 Dev Blog — Free Long-form content and deep-dives
- ✉️ Substack — Weekly frontend stories & curated resources
- 🧩 Portfolio — Projects, talks, and recognitions
- ✍️ Hashnode — Developer blog posts & tech discussions
- ✍️ Reddit**** — Developer blog posts & tech discussions
Comments
Loading comments…