Nothing interrupts a refactoring session quite like a runtime exception that crashes your entire application. If you have recently migrated to modern Angular patterns or started using the standalone API, you have likely encountered this specific error in your browser console:
Error: NG0203: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext.
This error is strictly enforced because it protects the integrity of Angular's Dependency Injection (DI) system. While the error message provides hints, fixing it requires understanding when Angular allows you to ask for dependencies and how to handle scenarios that fall outside those rules.
The Root Cause: What is an Injection Context?
To fix NG0203, you must understand how the inject() function works under the hood.
Unlike the traditional Constructor Injection pattern, inject() is a global function. It does not pass dependencies as arguments; it requests them from the currently active DI container.
For inject() to locate the correct dependency, Angular must know which component, directive, or service is asking for it. Angular tracks this by creating a temporary Injection Context.
- Creation Phase: When Angular instantiates a class (like a Component), it opens an Injection Context.
- Availability: While this context is open, you can call
inject(). This includes code running in field initializers and theconstructor. - Closure: As soon as the class instance is fully created, Angular closes the Injection Context.
The error occurs because you are calling inject() after the instance has been created. This typically happens inside lifecycle hooks (like ngOnInit), inside asynchronous callbacks (like setTimeout or subscribe), or within methods triggered by user events.
Scenario 1: Using inject() Inside Lifecycle Hooks or Methods
The most common mistake is attempting to use inject() inside a method or a lifecycle hook like ngOnInit.
The Problematic Code
In the example below, the developer attempts to lazy-load a service inside ngOnInit. This fails because ngOnInit runs after the component construction is complete. By this time, the Injection Context is closed.
import { Component, OnInit, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `...`
})
export class UserProfileComponent implements OnInit {
ngOnInit() {
// ❌ ERROR: NG0203
// The Injection Context is already closed.
const http = inject(HttpClient);
console.log('Component initialized');
}
}
The Solution: Field Initializers
The standard fix is to move the inject() call to the top level of the class properties. This guarantees the code runs during the instantiation phase, while the context is still open.
import { Component, OnInit, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `...`
})
export class UserProfileComponent implements OnInit {
// ✅ FIX: Move inject() to property initialization
private readonly http = inject(HttpClient);
ngOnInit() {
// You can now safely use this.http
console.log('Component initialized');
}
}
This pattern is cleaner, more readable, and explicitly declares dependencies at the top of your file.
Scenario 2: Dynamic Injection in Asynchronous Functions
There are edge cases where you cannot initialize the dependency immediately. Perhaps you are writing a utility function, or you need to resolve a service dynamically inside a callback.
Since callbacks (like those in setTimeout, subscribe, or then) run on the browser's event loop significantly later than the component creation, they have no access to the original Injection Context.
The Advanced Fix: runInInjectionContext
Angular provides a specific utility for this scenario: runInInjectionContext. To use this, you must capture the Injector instance during the valid context window and pass it to your asynchronous logic.
Here is a robust implementation of how to execute inject() dynamically:
import { Component, Injector, inject, runInInjectionContext, OnInit } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-admin-panel',
standalone: true,
template: `<button (click)="loadAdminData()">Load Admin</button>`
})
export class AdminPanelComponent implements OnInit {
// 1. Capture the Injector while the context is valid (during construction)
private readonly injector = inject(Injector);
ngOnInit() {
setTimeout(() => {
// ❌ Fails: Logic runs later on the event loop
// const user = inject(UserService);
// ✅ Works: We manually restore the context
runInInjectionContext(this.injector, () => {
const user = inject(UserService);
console.log('User service loaded lazily:', user);
});
}, 1000);
}
loadAdminData() {
// Example: Using context inside a click handler
runInInjectionContext(this.injector, () => {
const user = inject(UserService);
user.getAdminDetails();
});
}
}
Why This Works
- Capture: We grab the
Injectorusinginject(Injector)as a class property. - Restore: When the asynchronous code runs,
runInInjectionContexttells Angular: "Use this specific Injector to resolve anyinject()calls inside this function block."
Scenario 3: Functional Guards and Interceptors
In modern Angular applications (v16+), class-based guards (CanActivate) and interceptors are being replaced by functional variants.
The inject() function is the primary way to access services in these functions. However, if you extract logic into a separate helper function, that helper must be called synchronously within the guard function.
Valid Pattern for Functional Guards
import { CanActivateFn, Router, inject } from '@angular/router';
import { AuthService } from './auth.service';
// ✅ Valid: The guard function itself runs within an injection context
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
return router.parseUrl('/login');
};
If you refactor the logic into a standard utility function that calls inject() but call it outside the guard's execution stack, you will trigger NG0203.
Technical Summary
To ensure high stability in your Angular application, adhere to these rules regarding inject():
- Prefer Field Initialization: Always define your dependencies as class properties using
private service = inject(ServiceType). This is the most performance-optimized and type-safe approach. - Avoid Constructor Body: While
inject()technically works inside theconstructor() {}block, mixing parameter injection andinject()calls there can be confusing. Stick to field initializers. - Manual Context: If you must use
inject()inside a function that runs later (async, events), you are responsible for passing theInjectorand usingrunInInjectionContext.
By understanding the lifecycle of the Injection Context, you move from guessing why Dependency Injection fails to architecting predictable, reactive Angular applications.