Testingintermediate
Testing Asynchronous Code
Techniques and common pitfalls for correctly testing promise-based and callback-based asynchronous code, including fake timers.
Asynchronous tests fail in a specific way: they pass without having tested anything.
Testing async code without awaiting it properly is like a teacher collecting exam papers and grading the class immediately, without actually waiting for every student to finish writing — a few unfinished papers might get graded as blank or fail silently, even though the student was still legitimately in the middle of writing a correct answer when the papers were prematurely collected.
Key Concepts
1
// broken -- the test ends before the assertion runs
it('loads the user', () => {
fetchUser(1).then(u => expect(u.name).toBe('Asha'));
});2
Nothing is returned or awaited, so the test finishes immediately and passes. A wrong name never fails it.
3
The three correct forms.
it('loads the user', async () => {
const u = await fetchUser(1);
expect(u.name).toBe('Asha');
});4
it('loads the user', () =>
expect(fetchUser(1)).resolves.toMatchObject({ name: 'Asha' }));5
it('rejects for a missing user', () =>
expect(fetchUser(999)).rejects.toThrow('not found'));6
Return or await. That is the rule — a promise the test does not wait for is a promise the test does not
test.
7
expect.assertions guards the error path.
it('handles failure', async () => {
expect.assertions(1); // exactly one must run
try {
await failing();
} catch (e) {
expect(e.message).toBe('boom');
}
});expect.assertions
8
Without it, a function that stops throwing makes the catch never run — and the test passes.
catch
9
Fake timers remove real waiting.
jest.useFakeTimers();
const fn = jest.fn();
setTimeout(fn, 5000);
jest.advanceTimersByTime(5000);
expect(fn).toHaveBeenCalled();
jest.useRealTimers();10
A test with a real five-second wait is a test people skip.
11
Mixing fake timers and promises needs care. Advancing timers does not flush the microtask queue:
12
jest.advanceTimersByTime(1000);
await Promise.resolve(); // let pending promises settle13
Or jest.runAllTimersAsync() in recent versions.
jest.runAllTimersAsync()
14
For UI, wait for the result rather than a duration.
await screen.findByText('Loaded'); // retries until found
await waitFor(() => expect(fn).toHaveBeenCalled());15
await new Promise(r => setTimeout(r, 1000)); // flaky -- avoid16
A fixed delay is either too short on a slow machine or wasted time on a fast one. That is the main source of
flaky tests in CI.
17
What the interviewer is probing.1. "What is the most common async test bug?" Probing: the missing await. Stalls: "Timeouts."
Moves up: forgetting to return or await the promise, so the test finishes before the assertion
runs and passes regardless.
18
2. "How do you assert that a promise rejects?" Probing: the API. Stalls: "try/catch." *Moves
up:* await expect(fn()).rejects.toThrow(...), which fails if it resolves instead.
19
3. "How do you test a debounce without waiting?" Probing: fake timers. Stalls: "Sleep in the
test." Moves up: fake timers, advancing virtual time so a 300ms debounce is tested instantly.
20
4. "Why avoid arbitrary sleeps?" Probing: flakiness. Stalls: "They are simple." Moves up:
they are slow and still race — wait for the condition instead, with a retrying assertion.