All topics
Testingintermediate

Testing Asynchronous Code

Techniques and common pitfalls for correctly testing promise-based and callback-based asynchronous code, including fake timers.

Testing asynchronous code introduces a class of pitfalls that don't exist in synchronous tests, primarily because a test can finish and be marked 'passed' before an asynchronous operation it kicked off has actually completed, unless the test framework is explicitly told to wait for it — a subtle but common source of tests that appear to pass while silently never actually verifying 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
For promise-returning code, the fix is straightforward: return the promise from the test function itself (older style), or, far more commonly in modern code, mark the test function async and await the operation under test directly, exactly like you would in application code — Jest (and similar frameworks) automatically waits for a returned or awaited promise to settle before considering the test complete, whereas a test that fires off a promise-based operation without returning or awaiting it will finish immediately, potentially marking the test 'passed' before any of its assertions inside the promise's .then() even had a chance to run.
returnasyncawait.then()
2
For code that's expected to reject or throw, await expect(promise).rejects.toThrow('message') is the idiomatic Jest pattern for asserting an async operation fails with a specific error, mirroring the synchronous expect(fn).toThrow() pattern but adapted for the async case.
await expect(promise).rejects.toThrow('message')expect(fn).toThrow()
3
For code relying on setTimeout/setInterval, real timers make tests slow (actually waiting out real delays) and occasionally flaky (timing-dependent races); Jest's fake timers (jest.useFakeTimers()) replace the real timer functions with controllable mocks, letting a test synchronously 'fast-forward' virtual time (jest.advanceTimersByTime(1000)) to trigger timer callbacks instantly without actually waiting in real wall-clock time at all, making time-dependent code both fast and deterministic to test.
setTimeoutsetIntervaljest.useFakeTimers()jest.advanceTimersByTime(1000)
4
A related common pitfall is forgetting to properly await/return a nested asynchronous operation buried inside a test, which can cause a test's assertions to simply never execute at all (rather than fail) if the test function returns before that inner promise settles — since Jest has no way of knowing to wait for a promise it was never told about, and such a test can misleadingly show up as passing indefinitely, silently testing nothing, until someone happens to notice the missing coverage.