Asynchronous code — promises, async/await, callbacks, and timers — needs special handling in tests, or Jest may mark a test as passed before the async work even finishes. This lesson introduces the core problem and the tools Jest provides to solve it.
Why Async Code Needs Special Handling
By default, Jest considers a test finished as soon as its callback function returns. If that callback kicks off an asynchronous operation (like a promise) but returns immediately without waiting for it, Jest moves on to the next test before the async assertion ever runs — and a failing assertion inside that callback won't fail the test at all.
Jest gives you several ways to tell it "wait for this async work to finish before deciding pass or fail": returning a promise, using async/await, or accepting a done callback for older callback-style APIs.
// WRONG: Jest doesn't wait for the promise
test('fetches a user (broken)', () => {
fetchUser(1).then((user) => {
expect(user.name).toBe('Ada'); // this might never even run before the test "passes"
});
});
// RIGHT: returning the promise tells Jest to wait
test('fetches a user (correct)', () => {
return fetchUser(1).then((user) => {
expect(user.name).toBe('Ada');
});
});
The only difference is the return keyword — but it's the difference between a real test and a silently broken one.
Returning a promise is the original pattern; Jest waits for it to settle.
async/await is the clearest and most widely used approach today.
The done callback is for legacy callback-based code that has no promise to return.
Never combine done with async/await in the same test function — pick one pattern.
Async Testing Cheat Sheet
The core patterns for telling Jest to wait for async work.
Pattern
When to Use
return somePromise
Function under test returns a promise
async () => { await ... }
Most common; works with try/catch
(done) => { ...; done(); }
Callback-based APIs with no promise
await expect(p).resolves...
Asserting directly on a resolved promise
await expect(p).rejects...
Asserting directly on a rejected promise
The "Silently Passing" Trap
The most dangerous async testing mistake is a test that always shows green, even when the underlying code is broken, because the assertion inside an unreturned promise callback never gets a chance to fail the test. This can hide real bugs for a long time before anyone notices.
Combining async/await (or return) with expect.assertions(n) (covered in the expect() lesson) is the most reliable defense against this trap.
Catching This with a Lint Rule
The eslint-plugin-jest rule jest/valid-expect-in-promise and similar rules can catch unreturned promises inside test callbacks automatically, flagging the mistake before the test is even run.
Install eslint-plugin-jest and enable its recommended rule set.
jest/valid-expect-in-promise flags promises with expect() calls that aren't returned/awaited.
Automated linting catches this mistake far more reliably than manual code review.
Common Mistakes
Forgetting to return or await a promise inside a test, letting assertions run after the test already passed.
Mixing the done callback pattern with async/await in the same test.
Not using expect.assertions() in tests where an assertion might not run at all due to a logic bug.
Assuming a green test result means the async code was actually exercised correctly.
Key Takeaways
Jest needs to be told to wait for async work: via return, async/await, or a done callback.
An unreturned promise inside a test can let assertion failures pass silently.
async/await is the clearest, most recommended pattern for modern Jest tests.
Lint rules like jest/valid-expect-in-promise help catch this mistake automatically.
Pro Tip
Install eslint-plugin-jest in any serious project. Its rules catch unreturned promises and several other async testing mistakes long before they become a mysteriously "passing" but broken test.
You now understand the fundamentals of async testing. Next, dig into testing promise-returning functions specifically.