JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeTopicstestingTesting Asynchronous Code
Next
testing
beginner
10 min read

Testing Asynchronous Code

async
async-await
done-callback
fake-timers
jest
promises
resolves-rejects
testing
vitest

How to reliably test async operations including promises, async/await, timers, and error rejections using Jest and Vitest patterns that prevent false positives and flaky tests.

Key Points

1Async/Await Pattern

Mark the test function as async and await the operation. The test runner waits for the promise to settle and fails automatically on rejection.

2Resolves and Rejects Matchers

Use expect(promise).resolves and expect(promise).rejects for expressive inline assertions on promise outcomes. Always await the expression.

3Fake Timers

Control setTimeout and setInterval with jest.useFakeTimers() or vi.useFakeTimers() to test time-dependent code without real delays.

4Forgotten Await Pitfall

Forgetting to await or return a promise is the most common async testing bug -- the test passes before assertions actually run, creating a false positive.

What You'll Learn

  • Write reliable async tests using async/await, returned promises, and .resolves/.rejects
  • Use fake timers to test setTimeout and setInterval without real delays
  • Identify and avoid false positives caused by forgotten awaits in async tests

Deep Dive

Asynchronous code is everywhere in modern JavaScript applications -- API calls, timers, event handlers, and database queries all involve async operations. Testing this code correctly is critical because async tests can pass falsely if the test runner does not wait for the async operation to complete. Understanding the different patterns for handling async in tests is one of the most practical skills an interviewer can assess.

async/await (Recommended Approach)

The cleanest way to test async code is marking your test function as async and using await. The test runner waits for the returned promise to settle before moving on. If the awaited promise rejects, the test fails automatically. This mirrors how you write production async code, making tests easy to read.

JavaScript
test('fetches user data', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Alice');
});

Returning Promises

If you return a promise from your test function (without async/await), the test runner waits for it. This is less common now but useful when working with older codebases or specific library APIs.

JavaScript
test('fetches user data', () => {
  return fetchUser(1).then(user => {
    expect(user.name).toBe('Alice');
  });
});

The done Callback (Legacy Pattern)

Before async/await was widespread, tests accepted a done callback parameter. The test runner waits until done() is called. If done is never called, the test times out and fails. Calling done(error) explicitly fails the test. This pattern is error-prone and should be avoided in new code, but you may encounter it in legacy test suites.

Testing Rejections with .resolves/.rejects

Jest and Vitest provide .resolves and .rejects matchers that unwrap promises inline. These are expressive and pair well with await:

JavaScript
await expect(fetchUser(-1)).rejects.toThrow('Not found');
await expect(fetchUser(1)).resolves.toEqual({ name: 'Alice' });

Always await the .resolves/.rejects expression. Forgetting await is a common bug -- the test passes without actually checking the assertion because the promise is never awaited.

Fake Timers

When code uses setTimeout, setInterval, or Date.now(), you can use fake timers (jest.useFakeTimers() / vi.useFakeTimers()) to control time programmatically. Call jest.advanceTimersByTime(ms) or vi.advanceTimersByTime(ms) to fast-forward without waiting in real time. Always restore real timers in afterEach to prevent test pollution.

Common Pitfalls

The biggest mistake is forgetting to await or return the promise, which causes the test to pass before assertions run. Another pitfall is mixing done with async -- if your test function is async, do not use the done parameter as it leads to confusing timeout errors. Finally, avoid using expect.assertions(n) as a substitute for proper async handling; use it as an additional safety net.

Fun Fact

Before async/await, the 'done' callback pattern was borrowed from Node.js-style error-first callbacks. Mocha was the first popular test runner to support it, and Jest adopted the convention for backwards compatibility.

Learn These First

Unit Testing Fundamentals

beginner

Matchers and Assertions

beginner

Continue Learning

Mocking Functions, Modules, and APIs

intermediate

Test Lifecycle Hooks

beginner

Practice What You Learned

How do you test asynchronous code in Jest?
junior
async
Jest supports testing async code with: async/await (recommended), returning Promises, or using the done callback. For async/await, mark test as async and use await. Always ensure async tests complete properly or they'll timeout.
Next
Unit Testing Fundamentals
Next