Functions that return a Promise need to be tested in a way that waits for the promise to settle. This lesson focuses specifically on testing promise-returning functions with Jest's .resolves/.rejects matchers.
Testing with .resolves and .rejects
.resolves unwraps a promise's fulfilled value before applying a matcher, and .rejects unwraps a rejected promise's error. Both require await (or return) in front of the expect() call, since they are themselves asynchronous.
These modifiers are usually cleaner than manually chaining .then()/.catch() inside the test body, especially for simple resolve/reject assertions.
function fetchUser(id) {
if (id <= 0) return Promise.reject(new Error('Invalid id'));
return Promise.resolve({ id, name: 'Ada' });
}
test('resolves with a user for a valid id', async () => {
await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'Ada' });
});
test('rejects for an invalid id', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Invalid id');
});
Both tests use await before expect() because .resolves/.rejects return their own promise that must be waited on.
Alternative: Returning a .then() Chain
test('resolves with a user (alternative style)', () => {
return fetchUser(1).then((user) => {
expect(user).toEqual({ id: 1, name: 'Ada' });
});
});
Returning a .then() chain is an older but still valid pattern, predating .resolves/.rejects.
.resolves/.rejects are generally preferred today for their concise, matcher-friendly syntax.
Both approaches require return (or await) so Jest waits for the promise to settle.
For multiple assertions on the same resolved value, async/await with a variable is often clearest.
Testing Promises Cheat Sheet
Every common pattern for testing promise-based code.
const v = await promise; expect(v)...; expect(v)...
Multiple Assertions on the Same Resolved Value
When you need several assertions on one resolved value, awaiting the promise into a variable first (instead of chaining several .resolves calls) keeps the test more readable and avoids awaiting the same promise multiple times.
test('returns a fully-formed user object', async () => {
const user = await fetchUser(1);
expect(user.id).toBe(1);
expect(user.name).toBe('Ada');
expect(user).toHaveProperty('name');
});
Testing Code That Uses Promise.all()
When testing a function that awaits several promises concurrently with Promise.all(), you can mock each dependency independently and simply await the combined function's result as usual — Jest doesn't need special handling for Promise.all() itself.
Forgetting await before expect(promise).resolves.../.rejects....
Using .resolves on a promise you expect to reject (or vice versa), producing a confusing failure.
Chaining many .resolves calls on the same promise instead of awaiting it once into a variable.
Not testing the rejection path for functions that can realistically fail.
Key Takeaways
.resolves/.rejects unwrap a promise's outcome before applying a matcher.
Both require await or return since they return their own promise.
Returning a .then() chain is a valid, older alternative to .resolves/.rejects.
Awaiting into a variable is cleanest when making multiple assertions on one resolved value.
Pro Tip
Default to async/await with a plain expect() on an awaited variable for anything beyond a single simple assertion — it reads more naturally than chaining multiple .resolves calls.
You now know how to test promises thoroughly. Next, focus specifically on testing async/await functions and error handling with try/catch.