skip to content

In Angular testing, what is the difference between waitForAsync and fakeAsync, and when would a test use each?

level: juniorimportance: should knowfreq 55%

answer

  1. real time versus virtual time
  2. both live in a test zone
  3. one ends when tasks finish
  4. the other advances with tick

basics

~10 s

waitForAsync lets real time pass and ends the test when every async task it started has finished; fakeAsync runs the test on a virtual clock you advance with tick or flush. Both need zone.js.

solid answer

~40 s

Both come from `@angular/core/testing` and depend on `zone.js` plus `zone.js/testing`. `waitForAsync(fn)` wraps a test in an asynchronous test zone: real timers and promises run at real speed, and the test completes automatically once every async task started inside it has finished — it was the classic wrapper for `beforeEach` with `compileComponents()`. `fakeAsync(fn)` runs the body on a **virtual clock**: timers only fire when you call `tick(ms)` or `flush()`, and microtasks run with `flushMicrotasks()` or at the start of each `tick`, so a 300 ms debounce takes no real time. Today the testing guide calls `fakeAsync` no longer recommended and unusable with the Vitest runner; plain `async`/`await` has replaced `waitForAsync`.

code

ts · 12 lines
ts
import {fakeAsync, tick} from '@angular/core/testing';

it('fires after 300 ms of virtual time', fakeAsync(() => {
  let fired = false;
  setTimeout(() => (fired = true), 300);

  tick(299);
  expect(fired).toBe(false);

  tick(1);
  expect(fired).toBe(true);
}));

go deeper

for a junior

Know the one-line difference: waitForAsync waits for real async work, fakeAsync fakes time and you advance it with tick. Both need zone.js.

for a middle

Explain how tick drains microtasks, what flush returns, the end-of-test flush default, and why new Vitest projects cannot use either helper.

for a senior

Decide per suite: keep fakeAsync where zone.js stays, plan runner fake timers for zoneless specs, and replace waitForAsync with async/await as files are touched.

for a principal

Weigh the cost of keeping zone.js in the test environment only for fakeAsync against rewriting timing tests during a zoneless migration.

## Two zone-based helpers with opposite ideas of time Angular's testing package ships two wrappers for asynchronous tests. Both run the test body inside a special **zone** — zone.js's mechanism for tracking every timer, promise and event callback started inside it — and both therefore need `zone.js` and `zone.js/testing` loaded. | | `waitForAsync(fn)` | `fakeAsync(fn)` | |---|---|---| | Time | Real | Virtual; advances only when you say | | When the test ends | When all async tasks in the zone have finished | When the body returns (then pending timers are flushed by default) | | How you wait | Nothing to call; the zone waits for you | `tick(ms)`, `flush()`, `flushMicrotasks()` | | Test body style | Callbacks or promises | Synchronous-looking | | Typical historic use | `beforeEach(waitForAsync(() => ...compileComponents()))` | Debounces, delays, polling | ## waitForAsync: let real time pass `waitForAsync` wraps the test so that the runner does not consider it finished until every asynchronous operation started inside it has completed. A 300 ms `setTimeout` makes the test take 300 ms of wall-clock time. It dates from before `async`/`await` was everywhere. Its most common home was: ```ts beforeEach(waitForAsync(() => { TestBed.configureTestingModule({imports: [ProductSearch]}).compileComponents(); })); ``` Today the same thing is written with native `async`/`await` — `beforeEach(async () => { await TestBed.configureTestingModule(...).compileComponents(); })` — which needs no zone at all. If zone.js is missing, `waitForAsync` returns a function that rejects with a message saying zone.js is needed. ## fakeAsync: control a virtual clock `fakeAsync` replaces time with a fake clock owned by the test: - **Timers** (`setTimeout`, `setInterval`, and anything built on them, such as RxJS's `debounceTime`) do not fire by themselves. - `tick(ms)` advances the clock by `ms`, running every timer that falls due and draining **microtasks** (promise callbacks) at the start and after each timer callback. - `flush()` runs pending non-periodic timers until none remain and returns the simulated milliseconds that passed. - `flushMicrotasks()` runs only pending microtasks. - `Date` is patched to follow the fake clock, so code comparing `Date.now()` sees virtual time. The payoff is fast, deterministic tests of timing logic: a 300 ms debounce is checked in microseconds, and the test can assert both "not yet at 299 ms" and "now at 300 ms". Since zone.js 0.15, `fakeAsync` **flushes pending timers when the body returns** by default; pass `{flush: false}` to get the older behaviour of throwing if timers are still queued. ## Where each stands in 2026 The current testing guide is explicit about the direction: 1. `fakeAsync` "is no longer recommended". Prefer native async testing or the runner's fake timers. 2. `fakeAsync` **cannot be used with the Vitest runner**, the default for new CLI projects since v21, because no zone.js patch is applied for that runner. The API docs carry the same warning on `fakeAsync`, `tick`, `flush`, `flushMicrotasks` and `discardPeriodicTasks`. 3. `waitForAsync` has been superseded by `async`/`await` for waiting on real work. Both still appear throughout zone-based suites running on Karma/Jasmine, and interviewers ask about them because you will maintain those suites. ## Choosing in an existing zone-based suite - Waiting for real asynchronous work to finish (compilation, a stubbed promise): **`async`/`await`**, or `waitForAsync` if the suite already uses it. - Testing logic that depends on elapsed time (debounce, throttle, retry delays, timeouts): **`fakeAsync` + `tick`**. - A zoneless suite on Vitest: neither; use the runner's fake timers for time and `await` for everything else. ## Reading an older spec Much of the code interviewers show you comes from zone-based projects. Recognise the patterns: - `beforeEach(waitForAsync(() => { ... compileComponents(); }))` — equivalent to an `async` `beforeEach` that awaits `compileComponents()`. - `it('...', fakeAsync(() => { ...; tick(); fixture.detectChanges(); ... }))` — a test that controls time; the bare `tick()` advances zero milliseconds but still runs due timers and drains microtasks. - `flush()` near the end of a `fakeAsync` body — "run whatever is left", often redundant since the default end-of-test flush. - `async(...)` from `@angular/core/testing` in very old code — the name `waitForAsync` replaced; it is no longer exported. ## Common confusions - Thinking `waitForAsync` speeds anything up; it only waits. - Thinking `fakeAsync` works without zone.js; it throws saying `zone.js/testing` is needed. - Calling `tick()` outside a `fakeAsync` body; it has no fake clock to advance.

  • What happens to timers still pending when a fakeAsync test body returns?
    Since zone.js 0.15, `fakeAsync` flushes them by default when the body returns, so a leftover timeout does not fail the test. Passing `{flush: false}` restores the older behaviour: the test throws an error reporting how many timers or periodic timers are still in the queue.
  • Why can't a new Angular CLI project's tests use fakeAsync?
    New projects use the Vitest runner through the unit-test builder and no longer load zone.js for the app. The testing guide states that `fakeAsync` cannot be used with Vitest because no zone.js patch is applied for that runner. Use the runner's fake timers for time-dependent logic and `async`/`await` for everything else.

waitForAsync is sitting in the kitchen until every pot has finished cooking; fakeAsync is a cooking show where the host says 'forty minutes later' and pulls the finished dish out at once.

saying these in an interview costs you the question

  • waitForAsync makes timers fire instantly, like fakeAsync.
  • fakeAsync works in zoneless tests without zone.js loaded.
  • fakeAsync is the recommended approach for new Vitest-based specs.
  • In fakeAsync, promises resolve on their own without tick or flushMicrotasks.
  • waitForAsync is still required around compileComponents in modern specs.