Modern Angular
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 chamada | inject() aqui? | Por quê |
|---|---|---|
Field initializer (private x = inject(X)) | Funciona | Roda durante a construção, o contexto está aberto. |
| Corpo do constructor | Funciona | Ainda dentro da construção. |
Factory de useFactory / InjectionToken | Funciona | As factories rodam dentro do contexto de DI. |
Dentro de runInInjectionContext(injector, fn) | Funciona | O injector fica disponível pra aquele stackframe. |
Corpo de lifecycle hook (ngOnInit) | Estoura NG0203 | Roda depois que a instância foi criada. |
Callback de subscribe / .then / setTimeout | Estoura NG0203 | Trabalho adiado, o contexto fechou faz tempo. |
| Método de classe ou getter | Estoura NG0203 | Chamado depois da construção, fora da janela. |
Event handler (alvo de (click)) | Estoura NG0203 | Dispara 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.
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.
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.
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
- https://angular.dev/errors/NG0203
- https://angular.dev/api/core/inject
- https://angular.dev/api/core/runInInjectionContext
- https://angular.dev/guide/di/dependency-injection-context
- https://angular.dev/guide/di
- https://angular.dev/api/core/rxjs-interop/takeUntilDestroyed
- https://angular.dev/api/core/DestroyRef
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.
André Ramosdisponível para vagas remotas, UTC−3Entre em contato →