Skip to content
Angular ng testing 4 min read

Testing Asynchronous Code

Most non-trivial Angular code is asynchronous: timers, promises, RxJS observables, and HTTP calls all resolve on a future tick of the event loop. Tests, by contrast, run synchronously and finish in a heartbeat — so without help, an assertion would execute before the async work completed. Angular gives you two complementary tools for this: fakeAsync with tick, which lets you control virtual time and keep your test fully synchronous, and waitForAsync, which waits for real pending async tasks to settle. Knowing which one to reach for makes async tests fast, deterministic, and easy to read.

Why async tests are tricky

Consider a component that updates a signal after a setTimeout. If your test calls the method and immediately asserts, the timeout callback has not yet run, so the assertion fails. You could sprinkle setTimeout or rely on Jasmine’s done callback, but those tests become slow and flaky — they depend on real wall-clock time and the order of microtasks. Angular’s testing utilities remove that uncertainty by either simulating the passage of time or blocking until the work genuinely finishes.

fakeAsync and tick

fakeAsync wraps a test function and runs it inside a special zone where every asynchronous operation — setTimeout, setInterval, and resolved promises — is queued instead of executed. You then advance a virtual clock with tick(ms) to flush those tasks deterministically. Because no real time passes, the test stays synchronous: you can assert immediately after each tick.

import { fakeAsync, tick, TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { Component } from '@angular/core';

@Component({
  selector: 'app-greeter',
  standalone: true,
  template: `<p>{{ message() }}</p>`,
})
export class GreeterComponent {
  message = signal('loading...');

  greetLater() {
    setTimeout(() => this.message.set('Hello!'), 2000);
  }
}

describe('GreeterComponent (fakeAsync)', () => {
  it('updates the message after the timer fires', fakeAsync(() => {
    const fixture = TestBed.createComponent(GreeterComponent);
    const component = fixture.componentInstance;

    component.greetLater();
    expect(component.message()).toBe('loading...'); // timer not yet fired

    tick(2000); // advance virtual time by 2s
    expect(component.message()).toBe('Hello!');
  }));
});

The tick(2000) call instantly drains the 2-second timer without waiting. You can call tick() multiple times to step through a sequence of timers, or tick(0) to flush only microtasks (resolved promises).

Inside fakeAsync, leaving a timer or interval still pending when the function returns throws an error: “X timer(s) still in the queue.” This is intentional — it forces you to account for every scheduled task.

Flushing without counting milliseconds

Sometimes you do not know the exact delay. Use flush() to drain all pending macrotasks at once, or flushMicrotasks() for just the microtask queue. flush() returns the amount of virtual time it advanced.

import { fakeAsync, flush } from '@angular/core/testing';

it('resolves a promise chain', fakeAsync(() => {
  let result = '';
  Promise.resolve().then(() => (result = 'done'));

  flush();
  expect(result).toBe('done');
}));

Discarding leftover timers

For setInterval or fire-and-forget timers you cannot reasonably flush, call discardPeriodicTimers() (for intervals) at the end so the test does not error on pending tasks.

waitForAsync

waitForAsync (formerly async) runs the test in a zone that tracks all pending async work and waits for it to complete with real timers before resolving. Pair it with fixture.whenStable(), a promise that settles once the zone’s task queue is empty. Reach for this when you cannot — or do not want to — use virtual time, such as code that depends on real microtask ordering or third-party libraries that schedule their own tasks.

import { waitForAsync, TestBed } from '@angular/core/testing';

it('reflects async data once stable', waitForAsync(() => {
  const fixture = TestBed.createComponent(GreeterComponent);
  fixture.componentInstance.greetLater();

  fixture.whenStable().then(() => {
    fixture.detectChanges();
    expect(fixture.componentInstance.message()).toBe('Hello!');
  });
}));

Because it uses real timers, a waitForAsync test that waits on a setTimeout(…, 2000) actually takes two seconds — so prefer fakeAsync for timer-heavy logic.

Choosing between them

AspectfakeAsync + tickwaitForAsync + whenStable
TimeVirtual — instantReal wall-clock
ControlStep through timers preciselyWait for everything to settle
Test styleFully synchronous assertionsPromise-based (.then)
Best forTimers, debounce, polling, promisesReal async libraries, unknown delays
GotchaErrors on leftover timersSlow if real delays are long

Do not mix XHR-based HttpClient calls with tick blindly — those are not patched by the fake zone. For HTTP, use provideHttpClientTesting and the HttpTestingController, which lets you flush responses synchronously even inside fakeAsync.

Best Practices

  • Default to fakeAsync with tick/flush for anything timer- or promise-based; it keeps tests fast and deterministic.
  • Always advance the clock to drain every scheduled task, or use discardPeriodicTimers() for intervals, to avoid the “timers still in the queue” error.
  • Call fixture.detectChanges() after advancing time so the template reflects the updated state before you assert.
  • Reserve waitForAsync for code that genuinely needs real timers or unpatched async APIs.
  • Prefer tick(0) or flushMicrotasks() to resolve promise chains rather than guessing a millisecond value.
  • Keep each async test focused on one behavior — a single timer, one promise, one stream emission — so failures point at an obvious cause.
Last updated June 14, 2026
Was this helpful?