Modern Angular
NG0100 ExpressionChangedAfterItHasBeenCheckedError: por que ele aparece no Angular zoneless
NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked é o erro de dev-mode que todo dev Angular já viu. Depois de ir pra zoneless ou pro OnPush default do Angular 22, ele aparece em código e em teste que nunca tinham estourado. A causa mudou: em zoneless ele costuma dizer que um binding mudou sem notificar o Angular, que é o contrato do zoneless, e não um problema de timing de lifecycle que você empurra pro ngOnInit.
O erro, na íntegra
A mensagem é NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'X'. Current value: 'Y'. Ele só estoura em desenvolvimento. Depois de cada rodada de change detection, o Angular faz uma segunda passada pra conferir que nada mudou, e se um binding agora lê diferente do que ele acabou de setar, ele lança. A checagem existe pra pegar uma view num estado inconsistente.
As causas clássicas são conversa velha: um getter ou método que devolve um valor novo a cada chamada, um componente filho escrevendo num binding do pai, um valor setado no ngAfterViewInit depois que o pai já tinha sido checado. Se fosse esse o seu caso, o fix seria o de manual, mover a escrita pro ngOnInit ou pro ngAfterContentInit, e você não estaria lendo isto. O que mudou foi onde o erro aparece agora.
Por que ele está aparecendo agora
Os times relatam uma onda de NG0100 logo depois de subir pro Angular 21, inclusive em teste unitário que estava verde no dia anterior, e o Angular 22 aumenta o raio ao tornar o OnPush o default de change detection. Se você não mexeu no componente mas o erro surgiu, o regime de change detection embaixo dele mudou.
Zoneless e OnPush são mais rígidos que o default com Zone.js. Com o Zone, uma passada ampla de change detection atualizava os bindings mesmo sem ninguém pedir explicitamente. Sem o Zone, um binding só atualiza quando algo notifica o Angular: uma escrita de signal, uma mudança de input, o async pipe ou o markForCheck. A passada de verificação do dev-mode agora também pega os bindings que o scheduler não teria atualizado, que é justamente o código que dependia do Zone sem ninguém perceber.
O que o NG0100 quer dizer em zoneless
Num app com Zone.js, o NG0100 costumava ser um problema de timing: duas passadas, valores diferentes, conserta o lifecycle hook. Em zoneless ele carrega mais informação. O Angular lança quando um binding foi atualizado sem notificação, sem escrita de signal, sem markForCheck, sem async pipe. É a mesma classe de bug que aparece quando um app vai pra zoneless: o valor mudou, mas nada avisou o Angular pra agendar a atualização.
Então leia o NG0100 em zoneless como um aviso adiantado, não como amolação. Em desenvolvimento ele estoura; em produção, sem a checagem de dev-mode, o mesmo binding simplesmente renderizaria desatualizado e você ia caçar fantasma. O erro está te fazendo um favor ao nomear o binding antes de ir pro ar.
| De onde vem a mudança | Por que o zoneless lança NG0100 | Fix |
|---|---|---|
| Um timer ou callback muta um campo | O campo mudou, nada agendou a atualização | Guarde num signal, ou chame markForCheck |
| Um getter devolve valor novo a cada chamada | As duas passadas leem valores diferentes | Memoize, ou mova pra um computed signal |
| Um filho escreve num input do pai num hook | O pai já tinha sido checado nesta passada | Escreva antes, ou modele como signal que o pai lê |
| O status de um reactive form controla a view | statusChanges emitiu, a leitura no template não foi agendada | Faça a ponte do statusChanges pra um signal |
A armadilha do teste: fixture.detectChanges()
O lugar novo mais comum de bater no NG0100 é um teste. Em zoneless, chamar fixture.detectChanges() depois de uma mutação estoura ele, porque o TestBed exige que o componente seja compatível com OnPush e acusa um binding atualizado sem notificação. O hábito de chamar detectChanges() depois de toda mudança virou o gatilho, não o conserto, e ele vinha escondendo a notificação que faltava esse tempo todo.
Pare de forçar change detection. Conduza o componente como a produção conduz, escreva por um canal de verdade e espere a estabilidade, pra o teste exercitar o mesmo caminho de notificação que o app usa. Um teste que passa só porque você chamou detectChanges() é um teste que deixaria passar um bug de view desatualizada.
// Antes: força CD, estoura NG0100 em zoneless, esconde a notificação que falta
component.label = 'atualizado';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('atualizado');
// Depois: escreva pelo canal de verdade (um signal), aí espere a estabilidade
component.label.set('atualizado');
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('atualizado');O fix quase nunca é silenciar
O movimento errado é espalhar ChangeDetectorRef.detectChanges() ou embrulhar a atribuição em setTimeout até o erro calar. Isso esconde a inconsistência que a checagem achou, e em zoneless esconde uma view que vai renderizar desatualizada em produção. Silenciar o NG0100 troca um erro barulhento de dev-mode por um silencioso em produção.
O fix certo nomeia onde o valor mudou e roteia por algo que o Angular observa. Um campo que um timer muta vira um signal. Um getter que recalcula vira um computed. Um callback de terceiro que atualiza a UI ganha um markForCheck na borda, ou um adapter que escreve um signal. Os fixes clássicos de lifecycle hook ainda valem quando a causa é mesmo timing; os casos de zoneless precisam de uma notificação, não de um segundo detectChanges.
// Estoura NG0100 em zoneless: o campo muda, nada notifica o Angular
ngOnInit() {
setInterval(() => { this.elapsed = this.elapsed + 1; }, 1000);
}
// Resolvido: um signal é uma notificação; a atualização do template é agendada
readonly elapsed = signal(0);
ngOnInit() {
setInterval(() => this.elapsed.update((n) => n + 1), 1000);
}Artefato para reaproveitar
Depurando o NG0100 em zoneless
- Leia os valores Previous e Current na mensagem; eles nomeiam o binding que mudou.
- Pergunte o que atualizou esse binding, e se algo notificou o Angular (signal, markForCheck, async pipe).
- Mutação de campo por timer, callback ou terceiro: mova pra um signal, ou markForCheck na borda.
- Um getter que devolve valor novo a cada chamada: memoize ou use um computed signal.
- No teste, pare de chamar fixture.detectChanges() depois de mutação; escreva por canais de verdade e espere a estabilidade.
- Não silencie com detectChanges ou setTimeout; em zoneless isso esconde uma view que fica desatualizada em produção.
Perguntas frequentes
O que é o NG0100 ExpressionChangedAfterItHasBeenCheckedError?
Um erro só de desenvolvimento que o Angular lança quando um binding de template muda de valor depois que o change detection já rodou. Em dev mode o Angular faz uma segunda passada de verificação; se um binding agora lê diferente do que acabou de setar, ele lança pra acusar uma view inconsistente.
Por que o NG0100 começou depois que subi pro Angular 21 ou 22?
Zoneless (o default em projeto novo desde a v21) e o OnPush como default na v22 são mais rígidos que o default com Zone.js. Um binding que o Zone atualizava de qualquer jeito agora só atualiza quando algo notifica o Angular, e a checagem de dev-mode pega os que dependiam do Zone sem ninguém perceber.
Por que o fixture.detectChanges() estoura NG0100 em teste zoneless?
Em zoneless, o TestBed exige que o componente seja compatível com OnPush. Chamar fixture.detectChanges() depois de uma mutação acusa um binding atualizado sem notificação. Escreva por canais de verdade (um signal, um evento) e espere a estabilidade, em vez de forçar a detecção.
O NG0100 é um problema de produção?
O erro em si é só de desenvolvimento. Mas em zoneless ele costuma apontar um binding que mudou sem notificar o Angular, o que em produção renderizaria desatualizado sem erro nenhum. Consertar tira um bug de verdade, não só um aviso.
Fontes consultadas
- https://angular.dev/errors/NG0100
- https://angular.dev/guide/zoneless
- https://angular.dev/guide/testing
- https://angular.dev/guide/components/lifecycle
- https://github.com/angular/angular/issues/59082
- https://github.com/angular/angular-cli/issues/32047
- https://blog.angular.dev/announcing-angular-v21-57946c34f14b
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 →