---
title: "Por que seu async validator fica PENDING com os fake timers do Vitest"
description: "A falha silenciosa de tempo no Vitest: por que um async validator do Angular fica PENDING depois de uma migração de fakeAsync, por que o advanceTimersByTime não basta, e a linha exata entre uma Promise (precisa do avanço assíncrono) e RxJS mais HttpClient (não precisa)."
deck: "Esse não lança erro. O teste roda, nada quebra, e a assertion falha num valor que deveria estar lá: o control ainda está `PENDING`. É o primo silencioso do erro ProxyZone, e a correção é uma palavra só. [Os fake timers do Vitest](https://angular.dev/guide/testing/migrating-to-vitest) disparam timers mas não drenam os microtasks de Promise, então o resultado da validação nunca chega antes do seu expect rodar."
author: "André Ramos"
url: "https://andreramos.dev/pt/angular/angular-vitest-async-validator-pending/"
lang: "pt-BR"
type: "article"
---

# Por que seu async validator fica PENDING com os fake timers do Vitest

> Esse não lança erro. O teste roda, nada quebra, e a assertion falha num valor que deveria estar lá: o control ainda está `PENDING`. É o primo silencioso do erro ProxyZone, e a correção é uma palavra só. [Os fake timers do Vitest](https://angular.dev/guide/testing/migrating-to-vitest) disparam timers mas não drenam os microtasks de Promise, então o resultado da validação nunca chega antes do seu expect rodar.

## A falha que parece impossível

Não há erro de ProxyZone aqui. O spec roda, nada quebra, e então ele falha num valor que obviamente deveria estar setado. O async validator era pra marcar o control como inválido, mas `control.status` ainda está `PENDING` e `control.errors` é `null`. Nada na saída explica o porquê.

Isso aparece de duas formas: um teste de `fakeAsync` que você migrou no mecânico (trocou `tick` por `vi.advanceTimersByTime` e deu por encerrado), ou um teste novo que controla tempo com fake timers. De qualquer jeito, o timer disparou. O resultado é que ainda não está lá.

*Roda limpo, falha mesmo assim*

```ts
it('marca username em uso', () => {
  vi.useFakeTimers();
  const control = new FormControl('taken', {
    asyncValidators: [uniqueUsernameValidator(['taken'])],
  });

  vi.advanceTimersByTime(200);          // dispara o timer, não o microtask

  expect(control.status).toBe('INVALID'); // recebido: 'PENDING'
});
```

## Por quê: um timer não é um microtask

O `vi.advanceTimersByTime()` dispara timers, de forma síncrona. Um timer é um macrotask: `setTimeout`, `setInterval`, e os schedulers do RxJS que pegam carona neles. O debounce do async validator é um timer, então o avanço de fato o dispara.

Mas o resultado do validator é aplicado quando a Promise dele resolve, e a resolução de uma Promise é um microtask, não um timer. O avanço síncrono nunca drena a fila de microtasks, então na hora do seu expect, o callback da Promise que setaria o erro ainda não rodou. O control ainda está `PENDING`. A falha parece impossível porque o validator rodou sim. A resposta dele está só parada numa fila que você nunca esvaziou.

| Mecanismo | Tipo | Disparado pelo `advanceTimersByTime` (sync)? |
|---|---|---|
| `setTimeout`, `setInterval` | Macrotask (timer) | Sim |
| RxJS `debounceTime`, async scheduler | Macrotask (carona no `setInterval`) | Sim |
| `Promise.then`, resultado de async validator, `whenStable` | Microtask | Não, precisa do avanço assíncrono |

## A correção é uma palavra: Async

Troque `advanceTimersByTime` por `advanceTimersByTimeAsync`, faça o teste `async`, e dê `await`. A variante assíncrona avança o tempo e drena os microtasks no meio, então a Promise resolve e o resultado chega antes da sua assertion. Essa é a correção inteira.

*O avanço assíncrono drena o microtask*

```ts
it('marca username em uso', async () => {        // agora async
  vi.useFakeTimers();
  const control = new FormControl('taken', {
    asyncValidators: [uniqueUsernameValidator(['taken'])],
  });

  await vi.advanceTimersByTimeAsync(200);          // tempo E microtasks

  expect(control.status).toBe('INVALID');          // passa
  expect(control.errors).toEqual({ taken: true });
});
```

## É Promise, não async em geral

Aqui é onde as pessoas exageram na correção e começam a dar await em tudo. Não devem. Nem todo teste de cara assíncrona precisa do avanço assíncrono. Um stream RxJS que alimenta uma chamada `HttpClient` é síncrono dentro de um teste: o `vi.advanceTimersByTime` dispara o debounce, e o `HttpTestingController.flush()` aplica a resposta de forma síncrona. Não há Promise nessa cadeia, então não há microtask a drenar.

A armadilha é especificamente uma Promise: um async validator, um `.then`, um `whenStable()`, ou um método de service que retorna uma Promise. Viu um desses, pega o avanço assíncrono. Quando a cadeia é RxJS e `HttpClient` de ponta a ponta, o avanço síncrono e um `flush()` estão certos, e adicionar `await` só esconde o que está de fato acontecendo.

*RxJS mais HttpClient continua síncrono*

```ts
const httpMock = TestBed.inject(HttpTestingController);

vi.advanceTimersByTime(300);                 // dispara o debounce -> http.get
const req = httpMock.expectOne('/api/search?q=ab');
req.flush([{ id: 1, label: 'Abacaxi' }]);    // aplicado de forma síncrona, sem Promise

expect(seen).toEqual([[{ id: 1, label: 'Abacaxi' }]]);
```

## Como reconhecer em cinco segundos

O sinal é o formato da falha: o teste roda, nada quebra, e um valor está simplesmente faltando (`PENDING`, `null`, `undefined`) onde deveria estar setado. No instante em que você suspeita de tempo, faça uma pergunta. Há uma Promise na cadeia? Se sim, troque `advanceTimersByTime` por `await advanceTimersByTimeAsync`. Se o valor aparece, era isso.

Essa é a metade silenciosa do mesmo problema que o [erro ProxyZone](/pt/angular/angular-vitest-fakeasync-proxyzone-error) anuncia em alto e bom som, e os dois vêm do split que os [fake timers do Vitest](/pt/angular/vitest-fake-timers-vs-fakeasync-angular) deixam explícito: tempo é um trabalho, microtasks são outro.

## Artefato reaproveitável: Diagnosticando uma falha silenciosa de tempo no Vitest

- Sintoma: o teste roda, nada quebra, mas um valor está faltando (`PENDING`, `null`, `undefined`).
- Procure uma Promise na cadeia: async validator, `.then`, `whenStable`, service que retorna Promise.
- Se houver, faça o teste `async` e use `await vi.advanceTimersByTimeAsync(ms)`.
- Se for RxJS mais `HttpTestingController` de ponta a ponta, mantenha o avanço síncrono e o `flush()`; não há microtask a drenar.
- Ainda travado? Tente `await vi.runAllTimersAsync()` para drenar todos os timers pendentes e seus microtasks.
- Resete com `vi.useRealTimers()` no `afterEach`.

## Perguntas frequentes

### Por que meu async validator do Angular fica PENDING num teste Vitest?

Porque o `vi.advanceTimersByTime()` dispara o timer do debounce mas não drena o microtask de Promise que aplica o resultado da validação. O control resolve num microtask que não rodou antes da sua assertion, então fica `PENDING`. Use `await vi.advanceTimersByTimeAsync(ms)`, que avança o tempo e drena os microtasks.

### O advanceTimersByTime não funciona, o valor continua undefined. Por quê?

Ele funciona, mas só nos timers. Qualquer coisa aplicada por uma Promise (um async validator, um `.then`, `whenStable`) cai num microtask, e o avanço síncrono não drena microtasks. Troque pela variante assíncrona e dê `await`.

### Qual a diferença entre um timer e um microtask aqui?

Um timer (`setTimeout`, `setInterval`, e os schedulers do RxJS em cima deles) é um macrotask, e o `advanceTimersByTime` o dispara. A resolução de uma Promise é um microtask, drenado só pelo avanço assíncrono (`advanceTimersByTimeAsync`, `runAllTimersAsync`) ou por um `await` real.

### Meus testes de HttpClient precisam do advanceTimersByTimeAsync também?

Em geral não. Um stream RxJS alimentando `HttpClient` é síncrono num teste: avance o timer pro debounce, e o `HttpTestingController.flush()` aplica a resposta de forma síncrona. Você só precisa do avanço assíncrono se uma Promise estiver na cadeia, por exemplo um `.then` depois da resposta.

### O runAllTimers conserta um validator PENDING?

Não, use a versão assíncrona: `await vi.runAllTimersAsync()`. Os helpers de timer assíncronos drenam microtasks entre os timers, que é o que deixa a Promise do validator resolver. O `runAllTimers` síncrono tem o mesmo ponto cego do `advanceTimersByTime`.

## Fontes consultadas

- https://angular.dev/guide/testing
- https://angular.dev/guide/testing/migrating-to-vitest
- https://angular.dev/api/core/testing/TestBed
- https://vitest.dev/api/vi
- https://vitest.dev/guide/mocking
- https://blog.angular.dev/announcing-angular-v21-57946c34f14b

## Leia também

- [fakeAsync e o erro ProxyZone: consertando testes Angular no Vitest](https://andreramos.dev/pt/angular/angular-vitest-fakeasync-proxyzone-error/)
- [Fake timers do Vitest no Angular: o modelo mental depois do fakeAsync](https://andreramos.dev/pt/angular/vitest-fake-timers-vs-fakeasync-angular/)
- [Quando migrar uma suíte Angular para Vitest](https://andreramos.dev/pt/angular/move-angular-test-suite-to-vitest/)
