NG0203: por que o inject() falha fora do contexto de injeção

O NG0203 quer dizer que você chamou o inject() depois que o Angular terminou de montar a classe. O contexto de injeção fica aberto só durante a construção: num field initializer, no corpo do constructor, ou dentro do runInInjectionContext. Chame mais tarde, de um subscribe, de um setTimeout ou de um getter, e ele estoura. Fonte: angular.dev/errors/NG0203.

A janela, e quando ela fecha

A mensagem completa é inject() must be called from an injection context. O contexto de injeção é a janela de runtime em que o injector atual está ao alcance, e a doc é exata sobre quando ele existe: durante a construção de uma classe que o sistema de DI instancia, nos field initializers dessa classe, numa factory de useFactory ou de InjectionToken, e dentro de qualquer stackframe que roda via runInInjectionContext. Essa é a lista inteira.

A pegadinha é que a janela é estreita. Ela fica aberta enquanto o Angular monta a instância e fecha no instante em que a construção termina. A referência da API diz isso sem rodeio: chamadas ao inject() ficam proibidas depois que a instância já foi criada, em métodos, incluindo os lifecycle hooks. Então a pergunta nunca é *consigo injetar esse tipo*, porque o provider existe. A pergunta é *o contexto de injeção ainda estava aberto quando essa linha rodou*.

Onde o inject() funciona, e onde ele estoura

A maioria dos NG0203 vem de um número pequeno de lugares de chamada. O padrão se repete: código que roda de forma síncrona enquanto o Angular monta a classe passa, e código adiado pra um callback, um timer, um stream ou o corpo de um método já perdeu a janela.

Leia a tabela pelo lugar de chamada, não pelo tipo. O tipo é injetável em todas as linhas. O que muda é se a linha executa dentro da janela de construção ou depois dela.

Lugar de chamadainject() aqui?Por quê
Field initializer (private x = inject(X))FuncionaRoda durante a construção, o contexto está aberto.
Corpo do constructorFuncionaAinda dentro da construção.
Factory de useFactory / InjectionTokenFuncionaAs factories rodam dentro do contexto de DI.
Dentro de runInInjectionContext(injector, fn)FuncionaO injector fica disponível pra aquele stackframe.
Corpo de lifecycle hook (ngOnInit)Estoura NG0203Roda depois que a instância foi criada.
Callback de subscribe / .then / setTimeoutEstoura NG0203Trabalho adiado, o contexto fechou faz tempo.
Método de classe ou getterEstoura NG0203Chamado depois da construção, fora da janela.
Event handler (alvo de (click))Estoura NG0203Dispara na ação do usuário, muito depois da construção.

As armadilhas que parecem inofensivas

A primeira armadilha é o inject() dentro de um subscribe. O constructor parece certo, o lugar da chamada parece certo, mas o callback roda quando o valor chega, não quando a classe é montada, e a essa altura o contexto já fechou.

A segunda é o route guard async. Um CanActivateFn roda num contexto de injeção quando começa, então o primeiro inject() passa. Coloque um await antes do próximo e você saiu da janela síncrona: a doc afirma que o inject só dá pra usar de forma síncrona e não pode ser usado depois de nenhum await. A terceira é a silenciosa. Um getter privado ou um método auxiliar que chama inject() se parece com código de field, mas executa no acesso, muito depois da construção.

Três lugares de chamada que estouram NG0203ts
export class OrdersComponent {
  private readonly orders = inject(OrderService);

  ngOnInit() {
    this.orders.changes$.subscribe(() => {
      // NG0203: o callback roda depois da construção
      const logger = inject(LoggerService);
      logger.track('orders changed');
    });
  }

  // NG0203: o getter roda no acesso, não durante a construção
  get currency() {
    return inject(LOCALE_ID) === 'pt-BR' ? 'BRL' : 'USD';
  }
}

Os fixes, ranqueados

Comece no topo desta lista e só desça quando o caso te obrigar. A maior parte do NG0203 some no primeiro passo, porque a referência injetada não precisava ser resolvida tarde, e sim o código foi escrito tarde.

Primeiro, mova o inject() pra um field initializer ou capture a referência uma vez na construção e use depois. O field guarda a dependência; o callback usa o field. Segundo, quando você precisa mesmo resolver dentro de código adiado (um token escolhido em runtime, um injector por chamada), capture o Injector na construção e chame runInInjectionContext(injector, () => inject(X)) onde o trabalho roda. Terceiro, pro caso específico de cleanup de RxJS, o takeUntilDestroyed aceita um DestroyRef explícito, então dá pra capturar o inject(DestroyRef) uma vez e passar pro operador fora do contexto.

Eu recusaria o fix reflexo de embrulhar toda chamada tardia em runInInjectionContext. Ele silencia o NG0203, mas em geral significa que o design adiou uma decisão que era da construção. Quando vejo isso em revisão, pergunto o que de fato é dinâmico ali. Se a resposta é *nada*, a referência deveria ser um field, e não uma busca em runtime.

Capture na construção, use no callbackts
export class OrdersComponent {
  private readonly orders = inject(OrderService);
  // Capture uma vez, dentro da janela de construção:
  private readonly logger = inject(LoggerService);
  private readonly destroyRef = inject(DestroyRef);

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

O runInInjectionContext, e o await que quebra ele

Quando a busca é mesmo adiada, injete o Injector (ou o EnvironmentInjector) na construção e reabra o contexto onde você precisar. A assinatura é runInInjectionContext(injector, fn): ele roda a fn com aquele injector disponível, então o inject() funciona dentro da closure e devolve o resultado dela.

Tem um detalhe que engana muita gente, e vale dizer em voz alta: o contexto que o runInInjectionContext abre é síncrono. No momento em que você dá um await dentro da closure, você cai fora dele, e um inject() depois do await estoura NG0203 de novo. Então resolva tudo o que precisa antes do primeiro await, e só depois faça o trabalho assíncrono. Essa é a mesma regra que pega os route guards async, e é o motivo mais comum de um fix com runInInjectionContext ainda falhar.

Injete o Injector, reabra o contexto depoists
export class ReportExporter {
  private readonly injector = inject(Injector);

  exportLater() {
    setTimeout(() => {
      runInInjectionContext(this.injector, () => {
        // Resolva de forma síncrona, antes de qualquer await:
        const http = inject(HttpClient);
        const url = inject(REPORT_URL);
        http.get(url).subscribe();
      });
    }, 1000);
  }
}

Artefato para reaproveitar

Resolvendo o NG0203 de inject() fora do contexto de injeção

  • Confirme o lugar de chamada: um field initializer, o constructor ou uma factory está dentro da janela; um callback, método, getter ou event handler está fora dela.
  • Se a referência não é de fato dinâmica, mova o inject() pra um field initializer e use o field no callback.
  • Capture o inject(DestroyRef) na construção e passe pro takeUntilDestroyed pra dar unsubscribe fora do contexto.
  • Quando a resolução tem que ser adiada, injete o Injector na construção e embrulhe o código tardio em runInInjectionContext(injector, fn).
  • Dentro do runInInjectionContext, resolva todo inject() antes do primeiro await; o contexto é síncrono e um await te tira dele.
  • Trate um runInInjectionContext reflexo como cheiro de problema: se nada é dinâmico, a dependência é de um field, e não de uma busca em runtime.

Perguntas frequentes

O que o NG0203 quer dizer no Angular?

O NG0203 quer dizer que o inject() rodou fora de um contexto de injeção. A mensagem completa é 'inject() must be called from an injection context'. O contexto de injeção fica aberto só enquanto o Angular monta a classe (num field initializer, no constructor, numa factory ou dentro do runInInjectionContext) e fecha quando a construção termina.

Onde dá pra chamar o inject() sem estourar NG0203?

Num field initializer, no corpo do constructor, numa factory de useFactory ou InjectionToken, e dentro do runInInjectionContext. Ele estoura no corpo de lifecycle hooks, em callbacks de subscribe e setTimeout, em métodos de classe, em getters e em event handlers, porque esses rodam depois da construção.

Como eu uso o inject() dentro de um subscribe ou setTimeout?

Capture a dependência na construção (um field initializer) e use o field no callback. Se a resolução precisa acontecer tarde, injete o Injector na construção e chame runInInjectionContext(injector, () => inject(X)) onde o código adiado roda.

Por que o inject() ainda estoura NG0203 depois de um await?

O contexto de injeção, inclusive o que o runInInjectionContext abre, é síncrono. Depois de um await você já saiu dele, então um inject() mais tarde estoura. Resolva tudo o que precisa antes do primeiro await, e só depois faça o trabalho assíncrono.

Fontes consultadas

Modern Angular Playbook

Este artigo é um play.

O Playbook do Angular Moderno reúne o diagnóstico, a matriz de adoção, onze jogadas e o plano de 30 dias. Grátis, em inglês e português.

Você recebe os dois PDFs por email, pela lista do Dojo IA.

Abrir playbook →

André Ramosdisponível para vagas remotas, UTC−3Entre em contato →

Leia também

Erro · 8 min

NG0200: dependência circular na injeção de dependências

O que o NG0200 quer dizer, como ler a cadeia A -> B -> A que o Angular imprime, as três formas que produzem o ciclo, e os consertos em ordem, da reestruturação até o forwardRef como último recurso.

Ler artigo →

Erro · 7 min

NG0950: lendo um input obrigatório antes do Angular preencher

Por que o NG0950 do Angular dispara quando um signal input obrigatório é lido antes do binding, quando os inputs ficam disponíveis de fato, e os três consertos: ler no ngOnInit / computed / effect, dar um valor padrão, ou fazer o binding no pai.

Ler artigo →

Guia · 9 min

Facade Pattern no Angular: quando o componente sabe demais

Um guia de produção sobre Facade Pattern no Angular: quando criar, o que ela deve assumir, o que não deve esconder e como testar esse limite.

Ler artigo →