Ch. 4 · Angular

Angular Inject and Injection Contexts

Where inject() works and where it throws NG0203: constructors, field initializers and guards versus ngOnInit, timers and code after await.

~6 min readintermediateupdated Oct 7, 2026

inject() looks like an ordinary function you can call anywhere, and that’s exactly why it trips people up. It’s not a global lookup. It asks “which injector is currently creating something?” and resolves the token from there. If nothing is being created at that moment, there’s no injector to ask, and Angular throws NG0203.

Once you see it that way, the rules stop being a list to memorise. This note tests every common call site with Angular 20.3 under Node (the same rules apply in 19) and shows the patterns that come out of it, including the one interviewers like most: the injectSomething() helper.

What an injection context actually is

When Angular instantiates a component, a directive, a service or a provider factory, it sets an internal “current injector” for the duration of that synchronous creation, then clears it. inject() reads that current injector. That’s the whole mechanism.

So the question “can I call inject() here?” becomes “is Angular in the middle of creating something, right now, on this call stack?” A few cases where the answer is yes:

  • A class field initializer: private http = inject(HttpClient); runs during construction.
  • The constructor body.
  • A provider factory: useFactory: () => inject(Config).apiUrl, or an InjectionToken with a factory.
  • Functional guards, resolvers and interceptors. The router and HttpClient run them inside an injection context on purpose, which is why canActivate: [() => inject(Auth).isLoggedIn()] works.
  • Anything inside runInInjectionContext(injector, fn).

Testing every call site

Here’s a component and a couple of services, each trying inject(Clock) from a different place:

@Component({ selector: 'app-widget', template: '' })
export class Widget {
  clock = inject(Clock);           // field initializer
  injector = inject(Injector);

  constructor() {
    inject(Clock);                 // constructor
  }

  ngOnInit() {
    inject(Clock);                 // lifecycle hook
    runInInjectionContext(this.injector, () => inject(Clock));
  }
}

@Injectable({ providedIn: 'root' })
export class AsyncSvc {
  ready = this.init();
  private async init() {
    await Promise.resolve();
    inject(Clock);                 // after an await
  }
}
TypeScript

Results, with each failure shortened:

field initializer         ok
constructor               ok
ngOnInit                  NG0203
runInInjectionContext     ok
token factory             ok
after await               NG0203
inside setTimeout         NG0203
module top level          NG0203
injector.get(Clock)       ok (no context needed)
Text

The full message is worth knowing by sight:

NG0203: The `Clock` token injection failed. `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`.
Text

Why ngOnInit and await fail

ngOnInit surprises people because it’s “part of the component”. But Angular calls it later, during the first change detection pass, after construction has finished and the current injector has been cleared. Being a method on the class doesn’t matter. Timing does.

The await case is the same idea in a different shape. The code before the first await runs synchronously inside the constructor, so it’s in context. Everything after the await runs in a later microtask, when construction is long over. The function looks like one block, but it executes in two pieces. A setTimeout or subscribe() callback fails for the same reason.

The fix in both cases is to inject eagerly and use later:

export class AsyncSvc {
  private clock = inject(Clock);   // resolved during construction
  ready = this.init();

  private async init() {
    await loadSomething();
    return this.clock.now();       // just a field read, no inject()
  }
}
TypeScript

That’s the right answer most of the time. Fields are cheap, and they make a class’s dependencies visible at the top.

When you genuinely need to inject later

Sometimes you don’t know which token you need until later, or you want to create an effect or a toSignal() after some event. Capture the Injector during construction and use one of two tools:

private injector = inject(Injector);

openEditor(kind: 'text' | 'image') {
  // 1. Plain lookup, no context needed:
  const svc = this.injector.get(kind === 'text' ? TextEditor : ImageEditor);

  // 2. Run code that itself calls inject() (effects, toSignal, helpers):
  runInInjectionContext(this.injector, () => {
    effect(() => console.log(svc.state()));
  });
}
TypeScript

injector.get() is the simpler of the two and works anywhere. runInInjectionContext is for when the code you’re calling uses inject() internally and you can’t change it. Many Angular APIs that need an injector, like effect(), toSignal() and afterNextRender(), also accept an { injector } option directly, which is clearer than wrapping them.

Gotcha

Be careful which injector you capture. A component’s Injector can see that component’s providers and is tied to its lifetime. Running code with it after the component is destroyed gives you destroyed services and effects that never run. EnvironmentInjector (the root or a lazy route’s) outlives components, but can’t see component-level providers.

The injectX() helper pattern

inject() working in plain functions enables a pattern you’ll see in modern Angular code: small functions that wrap a bit of DI plus logic, used as field initializers.

export function injectRouteParam(name: string): Signal<string | null> {
  assertInInjectionContext(injectRouteParam);
  const route = inject(ActivatedRoute);
  return toSignal(route.paramMap.pipe(map((p) => p.get(name))), { initialValue: null });
}

export class OrderPage {
  orderId = injectRouteParam('id');
}
TypeScript

The helper only works if it is called from an injection context, so it inherits the rule. assertInInjectionContext makes misuse fail fast with a message naming the helper. When the test called a similar injectClock() from ngOnInit, the error read NG0203: injectClock() can only be used within an injection context such as a constructor, ..., which is far easier to debug than an error about a token the caller never mentioned.

The naming convention (inject prefix) is the signal to readers that the function has this constraint. Interviewers who ask about “composable” or “functional” DI in Angular are usually fishing for this pattern.

NG0203 when your code looks correct

Occasionally the call site is fine and NG0203 still appears. The usual culprit is two copies of @angular/core in the bundle, typically from npm link on a local library or a library that lists Angular under dependencies instead of peerDependencies. The library’s inject() reads the “current injector” variable of its own copy of Angular, which the app’s copy never set. Running npm ls @angular/core shows the duplicate, and the fix is to make the library use the app’s copy (peer dependency, or preserveSymlinks for linked builds).

Another quiet source is takeUntilDestroyed() with no argument. It calls inject(DestroyRef) internally, so it only works in a constructor or field initializer. Inside ngOnInit you have to pass the DestroyRef you captured earlier: takeUntilDestroyed(this.destroyRef).

Constructor parameters versus inject()

Both forms resolve dependencies the same way, from the same injector, with the same hierarchy. inject() has practical advantages: it works with inheritance without passing arguments through super(...), it works in functions (guards, interceptors, helpers), and it plays well with TypeScript’s newer class field semantics. Constructor injection still works and isn’t deprecated. A common follow-up is “can you mix them?” Yes, freely, in the same class.

Interview practice

A colleague writes a service that loads feature flags on startup. It works in development but in a unit test it throws NG0203. What’s wrong, and how would you fix it?

@Injectable({ providedIn: 'root' })
export class Flags {
  flags = signal<Record<string, boolean>>({});

  async load() {
    const config = await fetch('/config.json').then((r) => r.json());
    const http = inject(HttpClient);
    this.flags.set(await firstValueFrom(http.get<Record<string, boolean>>(config.flagsUrl)));
  }
}
TypeScript

Answer

inject(HttpClient) runs after an await, so it executes in a later microtask when no injection context exists. I’d guess it “worked” in development only because load() was never reached that way, or the error was swallowed by an unhandled promise rejection; either way it’s broken, not environment-specific.

The fix is to resolve the dependency during construction: private http = inject(HttpClient); as a field, then use this.http inside load(). I’d also replace the raw fetch with this.http so the request goes through interceptors and can be mocked in tests with provideHttpClientTesting(). If the goal is to load flags before the app renders, I’d call it from provideAppInitializer(() => inject(Flags).load()) in appConfig. That callback runs in an injection context, but the important part is still that inject() happens before the first await, not after.

Further reading

More in Angular

esc