Skip to main content

Fixing Angular Error NG0203: inject() Must Be Called From an Injection Context

 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.

  1. Creation Phase: When Angular instantiates a class (like a Component), it opens an Injection Context.
  2. Availability: While this context is open, you can call inject(). This includes code running in field initializers and the constructor.
  3. 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 setTimeoutsubscribe, 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

  1. Capture: We grab the Injector using inject(Injector) as a class property.
  2. Restore: When the asynchronous code runs, runInInjectionContext tells Angular: "Use this specific Injector to resolve any inject() 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():

  1. 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.
  2. Avoid Constructor Body: While inject() technically works inside the constructor() {} block, mixing parameter injection and inject() calls there can be confusing. Stick to field initializers.
  3. Manual Context: If you must use inject() inside a function that runs later (async, events), you are responsible for passing the Injector and using runInInjectionContext.

By understanding the lifecycle of the Injection Context, you move from guessing why Dependency Injection fails to architecting predictable, reactive Angular applications.