skip to content

In Angular, where does an injection context exist, and why does calling inject() inside ngOnInit throw NG0203?

level: middleimportance: must knowfreq 60%

answer

  1. a current injector, set temporarily
  2. construction time only
  3. factories and token factories too
  4. framework-run functions like guards
  5. hooks run after construction

basics

~20 s

An injection context exists while Angular creates a DI-managed instance (field initializers, constructor), inside provider and InjectionToken factories, and in functions the framework runs in context, such as guards. ngOnInit runs after construction, when no context is active, so inject() throws NG0203.

solid answer

~40 s

`inject()` has no injector argument; it uses whichever injector Angular has made *current* for the running code. Angular sets that while it constructs a component, directive or service — so field initializers and the constructor body qualify — while it runs a `useFactory` function or an `InjectionToken` factory, and while it runs code wrapped in `runInInjectionContext`, which is how the router runs functional guards and resolvers. When construction finishes, the previous state is restored. `ngOnInit` is called later by change detection, as a normal method call, so nothing is current and `inject()` throws NG0203: "`inject()` function must be called from an injection context". The fix is to move the call to a field initializer or the constructor, keep the reference, and use it in the hook.

code

ts · 27 lines
ts
import { Component, Injectable, OnInit, inject, signal } from '@angular/core';
import { ActivatedRoute, CanActivateFn, Router } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class OrdersFeature {
  readonly enabled = signal(true);
}

@Component({
  selector: 'app-order-detail',
  template: `<p>Order {{ orderId }}</p>`,
})
export class OrderDetail implements OnInit {
  private readonly route = inject(ActivatedRoute); // OK: field initializer
  protected orderId = '';

  ngOnInit(): void {
    // const router = inject(Router); // NG0203: hooks run after construction
    this.orderId = this.route.snapshot.paramMap.get('id') ?? '';
  }
}

// OK: the router runs functional guards inside an injection context.
export const ordersEnabledGuard: CanActivateFn = () => {
  const router = inject(Router);
  return inject(OrdersFeature).enabled() || router.parseUrl('/');
};

go deeper

for a junior

Remember the safe places: field initializers and the constructor. Know that hooks and event handlers are not safe for inject().

for a middle

Explain the context as a temporarily set current injector during construction, factories and runInInjectionContext, and read NG0203 correctly.

for a senior

Trace NG0203 through helpers and callbacks to the frame that left the context, and choose between moving the call and capturing an injector.

for a principal

Establish conventions that keep injection at construction time, so dependency graphs stay visible and helpers stay testable.

## What an injection context is The `inject()` function takes a token but no injector. To resolve the token it needs to know *which* injector to ask — the component's element injector, a route's environment injector, the root. Angular keeps that answer in an internal "current injector" slot. The slot is filled only while Angular itself is running code that should be able to inject, and it is restored as soon as that code returns. Any stack frame running while the slot is filled is said to be in an **injection context**. This is a property of the **synchronous call stack**, not of the class or the file. The same line of code can be inside a context when called from a constructor and outside one when called from a click handler. ## Where a context exists Angular's guide lists these places: 1. **Construction of a DI-created class** — the constructor of a component, directive, pipe or service that Angular instantiates. 2. **Field initializers** of such classes, which run as part of construction. 3. **Provider factories** — a `useFactory` function, or the factory of an `@Injectable`. 4. **`InjectionToken` factories** — the `factory` option of a token. 5. **Code run inside `runInInjectionContext(injector, fn)`**, for as long as `fn` runs synchronously. The fifth item is how framework APIs extend the context to plain functions. The router, for example, runs functional guards and resolvers through `runInInjectionContext` with the route's environment injector, which is why `inject(AuthService)` at the top of a `CanActivateFn` works. ## Why `ngOnInit` is outside Lifecycle hooks are ordinary method calls that change detection makes **after** the instance exists: | Moment | Context active? | `inject()` | |---|---|---| | Field initializer | yes | works | | Constructor body | yes | works | | `ngOnInit`, `ngAfterViewInit`, other hooks | no | NG0203 | | Event handler such as `(click)` | no | NG0203 | | `setTimeout`, `Promise.then`, `subscribe` callbacks | no | NG0203 | | After an `await` inside construction-time code | no | NG0203 | The development-mode message spells out the rule: "`inject()` function must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`". ## Fixing NG0203 - **Move the call.** In almost every case the dependency can be injected in a field initializer and used later: `private readonly router = inject(Router)` and then `this.router.navigate(...)` in the hook. - **Capture an injector when the choice is dynamic.** If the token is only known later, inject `Injector` in a field and wrap the later code in `runInInjectionContext(this.injector, () => ...)`. - **Do not reach for global workarounds.** Storing an injector in a module-level variable to call `.get()` anywhere hides dependencies and breaks when several applications or tests share a page. ## Framework APIs that open a context for you The same mechanism explains why some Angular APIs accept plain functions that call `inject()`. The router is the familiar case: it runs functional guards and resolvers inside `runInInjectionContext` with the route's environment injector, so `CanActivateFn` and `ResolveFn` bodies can call `inject()` at their top. Other APIs go the other way and *require* a context because they need an injector themselves — they read it from the current context and throw NG0203 when there is none. In both directions the rule is identical: the context covers only the synchronous body of the function being run. ## Diagnosing from the stack trace The error is thrown at the `inject()` call, so the stack trace points at the line. The useful question is *who called this frame*: - a lifecycle hook or event handler — move the call to a field; - a callback such as a timer, promise or subscription — resolve before scheduling; - a helper function — check where the helper itself was called from; helpers that call `inject()` inherit the rule and must be called at construction time. ## Why Angular works this way Making the injector ambient is what lets `inject()` be a plain function usable in fields, factories and guards without passing an injector around. The price is that the context must be well-defined, and a synchronous stack frame is the only boundary JavaScript gives Angular for free. Asynchronous callbacks run on fresh stacks, long after the slot was restored, so they cannot see it.

  • Does providing a service with providedIn: 'root' let you call inject() for it anywhere?
    No. `providedIn` decides which injector can supply the service; the injection context decides whether `inject()` knows which injector to ask at all. Outside a context, `inject()` throws NG0203 for any token, however it is provided.
  • Why can a functional route guard call inject() even though it is just an exported arrow function?
    Because the router invokes it inside `runInInjectionContext`, using the route's environment injector. The arrow function is in context only for that synchronous call; if the guard awaited something and then called `inject()`, the later call would fail.

saying these in an interview costs you the question

  • inject() works anywhere inside a component class, including hooks
  • Services marked providedIn root can be injected outside a context
  • The injection context lasts for the lifetime of the component
  • Functional guards need their dependencies passed as parameters
  • NG0203 means the requested service has no provider