Jest has some built-in magic for handling asynchronous code, making it feel almost synchronous to write tests for.

Let’s see it in action. Imagine we have a simple function that fetches data after a delay:

// src/fetchData.js
function fetchData(shouldSucceed = true) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldSucceed) {
        resolve({ data: 'some fetched data' });
      } else {
        reject(new Error('Failed to fetch'));
      }
    }, 100);
  });
}

module.exports = fetchData;

Now, let’s test this with Jest, first using Promises and then async/await.

Testing with Promises

When your asynchronous function returns a Promise, Jest expects your test to return a Promise as well. Jest will wait for the returned Promise to resolve before finishing the test.

// src/fetchData.test.js
const fetchData = require('./fetchData');

test('fetches data successfully', () => {
  // The test returns the promise from fetchData
  return fetchData().then(data => {
    expect(data).toEqual({ data: 'some fetched data' });
  });
});

test('handles fetch errors', () => {
  // We expect the promise to be rejected
  expect.assertions(1); // Ensure the reject path is actually hit
  return fetchData(false).catch(error => {
    expect(error.message).toBe('Failed to fetch');
  });
});

In the first test, return fetchData().then(...) tells Jest: "Hey, this test involves an asynchronous operation. Wait for this Promise to resolve, and then run your assertions inside the .then() block." If the Promise rejects, the test will fail.

The second test uses .catch() to handle the rejection. expect.assertions(1) is crucial here. Without it, if fetchData(false) resolved unexpectedly, the .catch() block would never run, and the test would pass without any assertions being made, which is incorrect. expect.assertions() guarantees that a specific number of assertions will be called.

Testing with async/await

async/await is syntactic sugar over Promises, and Jest fully supports it. It makes asynchronous code look and behave more like synchronous code, which is much easier to read and write.

// src/fetchData.test.js (continued)
const fetchData = require('./fetchData');

test('fetches data successfully with async/await', async () => {
  const data = await fetchData();
  expect(data).toEqual({ data: 'some fetched data' });
});

test('handles fetch errors with async/await', async () => {
  expect.assertions(1);
  try {
    await fetchData(false);
  } catch (error) {
    expect(error.message).toBe('Failed to fetch');
  }
});

When you mark a test function with async, you can use await inside it. await fetchData() pauses the execution of the test function until the fetchData() Promise resolves. The resolved value is then assigned to data. If the Promise rejects, await throws an error, which can be caught by a try...catch block.

Just like with Promises, if your async test function doesn’t await any Promises (or return one), Jest will run it synchronously and move on immediately. If an async test throws an unhandled error (i.e., not caught by try...catch), Jest will interpret it as a test failure.

The mental model is simple:

  1. Promises: If your test uses Promises, return the Promise from the test function. Jest waits for it.
  2. async/await: Mark your test function async and use await for your asynchronous operations. Jest waits for the async function to complete.

The core problem this solves is preventing tests from finishing before the asynchronous operations they are testing have completed. Without these mechanisms, a test might assert against a state that hasn’t been updated yet, or it might miss a rejection entirely.

Jest’s expect.assertions() is a powerful safeguard for tests involving asynchronous rejections or when you want to be absolutely sure a certain code path was executed. It prevents false positives where a test passes because the asynchronous code failed to execute as expected, and thus no assertions were ever run.

A common pitfall is forgetting return when testing Promises, or forgetting async on the test function when using await. In both cases, Jest will not wait for the asynchronous operation, leading to tests that pass incorrectly because they finish too early.

When using async/await with expect.assertions(), you often place expect.assertions() at the very beginning of the async test function, or within the try block if you’re testing a successful resolution. If you are testing a rejection, place it before the try...catch block or within the catch block, depending on what you want to guarantee. The key is that the assertion count must match the number of assertions you expect to be called.

The next step is often dealing with mocks and timers in asynchronous tests, which Jest also provides robust utilities for.

Want structured learning?

Take the full Jest course →