NG0203: Why inject() Fails Outside an Injection Context

NG0203 means you called inject() after Angular finished building the class. The injection context is open only during construction: in a field initializer, in the constructor body, or inside runInInjectionContext. Call it later, from a subscribe, a setTimeout, a getter, or an event handler, and it throws. Source: angular.dev/errors/NG0203.

The window, and when it closes

The full message is inject() must be called from an injection context. The injection context is the runtime window where the current injector is reachable, and the docs are exact about when it exists: during construction of a class the DI system instantiates, in the field initializers of that class, in a useFactory or InjectionToken factory, and inside any stackframe run through runInInjectionContext. That is the whole list.

The trap is that the window is narrow. It is open while Angular is building the instance and closed the moment construction finishes. The API reference says it plainly: calls to inject() are disallowed after a class instance was already created, in methods including lifecycle hooks. So the question is never *can I inject this type*; the provider exists. The question is *was the injection context still open when this line ran*.

Where inject() works, and where it throws

Most NG0203 reports come from one of a short list of call sites. The pattern is consistent: code that runs synchronously while Angular builds the class is fine, and code deferred to a callback, a timer, a stream, or a method body has already missed the window.

Read the table by call site, not by type. The type is injectable in every row. What changes is whether the line executes inside the construction window or after it.

Call siteinject() here?Why
Field initializer (private x = inject(X))WorksRuns during construction, the context is open.
Constructor bodyWorksStill inside construction.
useFactory / InjectionToken factoryWorksFactories run inside the DI context.
Inside runInInjectionContext(injector, fn)WorksThe injector is made available for that stackframe.
Lifecycle hook body (ngOnInit)Throws NG0203Runs after the instance was created.
subscribe / .then / setTimeout callbackThrows NG0203Deferred work, the context closed long ago.
Class method or getterThrows NG0203Called after construction, outside the window.
Event handler ((click) target)Throws NG0203Fires on user action, well past construction.

The traps that look harmless

The first trap is inject() inside a subscribe. The constructor looks fine, the call site looks fine, but the callback runs when the value arrives, not when the class is built, and by then the context is closed.

The second is the async route guard. A CanActivateFn runs in an injection context when it starts, so the first inject() is fine. Put an await before the next one and you have crossed out of the synchronous window: the docs state inject is only usable synchronously and cannot be used after any await points. The third is the quiet one: a private getter or a helper method that calls inject(). It reads like field code, but it executes on access, long after construction.

Three call sites that throw NG0203ts
export class OrdersComponent {
  private readonly orders = inject(OrderService);

  ngOnInit() {
    this.orders.changes$.subscribe(() => {
      // NG0203: the callback runs after construction
      const logger = inject(LoggerService);
      logger.track('orders changed');
    });
  }

  // NG0203: a getter runs on access, not during construction
  get currency() {
    return inject(LOCALE_ID) === 'pt-BR' ? 'BRL' : 'USD';
  }
}

The fixes, ranked

Start at the top of this list and only move down when the case forces you to. Most NG0203 disappears at step one, because the injected reference did not need to be resolved late; the code was just written late.

First, move the inject() to a field initializer or capture the reference once at construction and use it later. The field holds the dependency; the callback uses the field. Second, when you genuinely need resolution inside deferred code (a dynamically chosen token, a per-call injector), capture the Injector at construction and call runInInjectionContext(injector, () => inject(X)) where the work runs. Third, for the specific RxJS-cleanup case, takeUntilDestroyed accepts an explicit DestroyRef, so you can capture inject(DestroyRef) once and pass it into the operator outside the context.

I would refuse the reflex fix of wrapping every late call in runInInjectionContext. It silences NG0203, but it usually means the design deferred a decision that belonged at construction. When I see it in review, I ask what is actually dynamic. If the answer is *nothing*, the reference should be a field, not a runtime lookup.

Capture at construction, use in the callbackts
export class OrdersComponent {
  private readonly orders = inject(OrderService);
  // Capture once, in the construction window:
  private readonly logger = inject(LoggerService);
  private readonly destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.orders.changes$
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => this.logger.track('orders changed'));
  }
}

runInInjectionContext, and the await that breaks it

When the lookup truly is deferred, inject the Injector (or EnvironmentInjector) at construction and reopen the context where you need it. The signature is runInInjectionContext(injector, fn): it runs fn with that injector available, so inject() works inside the closure and returns its result.

There is one edge that fools people, and it is worth saying out loud: the context runInInjectionContext opens is synchronous. The moment you await inside the closure, you fall out of it, and an inject() after the await throws NG0203 again. So resolve everything you need before the first await, then do the async work. This is the same rule that bites async route guards, and it is the single most common reason a runInInjectionContext fix still fails.

Inject the Injector, reopen the context laterts
export class ReportExporter {
  private readonly injector = inject(Injector);

  exportLater() {
    setTimeout(() => {
      runInInjectionContext(this.injector, () => {
        // Resolve synchronously, before any await:
        const http = inject(HttpClient);
        const url = inject(REPORT_URL);
        http.get(url).subscribe();
      });
    }, 1000);
  }
}

Reusable artifact

Resolving NG0203 inject() outside an injection context

  • Confirm the call site: a field initializer, constructor, or factory is inside the window; a callback, method, getter, or event handler is outside it.
  • If the reference is not actually dynamic, move inject() to a field initializer and use the field in the callback.
  • Capture inject(DestroyRef) at construction and pass it into takeUntilDestroyed to unsubscribe outside the context.
  • When resolution must be deferred, inject(Injector) at construction and wrap the late code in runInInjectionContext(injector, fn).
  • Inside runInInjectionContext, resolve every inject() before the first await; the context is synchronous and an await drops you out of it.
  • Treat a reflex runInInjectionContext as a smell: if nothing is dynamic, the dependency belongs in a field, not a runtime lookup.

FAQ

What does NG0203 mean in Angular?

NG0203 means inject() ran outside an injection context. The full message is 'inject() must be called from an injection context'. The injection context is open only while Angular builds the class (in a field initializer, the constructor, a factory, or inside runInInjectionContext) and closes once construction finishes.

Where can inject() be called without throwing NG0203?

In a field initializer, in the constructor body, in a useFactory or InjectionToken factory, and inside runInInjectionContext. It throws in lifecycle hook bodies, subscribe and setTimeout callbacks, class methods, getters, and event handlers, because those run after construction.

How do I use inject() inside a subscribe or setTimeout?

Capture the dependency at construction (a field initializer) and use the field in the callback. If resolution must happen late, inject the Injector at construction and call runInInjectionContext(injector, () => inject(X)) where the deferred code runs.

Why does inject() still throw NG0203 after an await?

The injection context, including the one runInInjectionContext opens, is synchronous. After an await you have left it, so a later inject() throws. Resolve everything you need before the first await, then do the async work.

Sources checked

Modern Angular Playbook

This article is one play.

The Modern Angular Playbook collects the diagnostic, the adoption matrix, eleven plays, and the 30-day plan. Free, in English and Portuguese.

You get both PDFs by email, through the Dojo IA list.

Open playbook →

André Ramosavailable for remote roles, UTC−3Get in touch →

Related

Error · 8 min

NG0200 Circular Dependency in DI: Reading the Chain and Breaking It

What NG0200 Circular Dependency in DI means, how to read the A -> B -> A chain Angular prints, the three causes that produce it, and the fixes ranked from restructuring down to forwardRef as a last resort.

Read article →

Error · 7 min

NG0950: Reading a Required Input Before Angular Sets It

Why Angular's NG0950 fires when a required signal input is read before binding, when inputs actually become available, and the three fixes: read it in ngOnInit / computed / effect, give it a default, or bind it in the parent.

Read article →

Guide · 9 min

Facade Pattern in Angular: When Components Know Too Much

A production Angular guide to the Facade Pattern: when to create one, what it should own, what it should not hide, and how to test the boundary.

Read article →