Testing Async Code
Modern JavaScript is full of asynchronous operations: API calls, file operations, timers. Jest provides several ways to test async code effectively.
Testing Promises
Using async/await (Recommended)
The cleanest way to test promises:
// Function that returns a promise
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// Test with async/await
describe('fetchUser', () => {
it('returns user data', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});
});
Using .resolves/.rejects
For simpler assertions:
function fetchData() {
return Promise.resolve({ data: 'result' });
}
function fetchWithError() {
return Promise.reject(new Error('Network error'));
}
describe('promise matchers', () => {
it('resolves with data', () => {
// Note: return the expectation!
return expect(fetchData()).resolves.toEqual({ data: 'result' });
});
it('rejects with error', () => {
return expect(fetchWithError()).rejects.toThrow('Network error');
});
// Or with async/await
it('resolves with data (async)', async () => {
await expect(fetchData()).resolves.toEqual({ data: 'result' });
});
});
Using .then() (Less Common)
it('returns data', () => {
return fetchData().then((data) => {
expect(data).toEqual({ data: 'result' });
});
});
Testing Rejected Promises
Multiple ways to test errors:
async function failingOperation() {
throw new Error('Operation failed');
}
describe('error handling', () => {
// Method 1: rejects matcher
it('rejects with error message', async () => {
await expect(failingOperation()).rejects.toThrow('Operation failed');
});
// Method 2: try/catch
it('throws expected error', async () => {
try {
await failingOperation();
fail('Expected error to be thrown');
} catch (error) {
expect(error.message).toBe('Operation failed');
}
});
// Method 3: expect.assertions (ensure catch runs)
it('handles rejection', async () => {
expect.assertions(1);
try {
await failingOperation();
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
});
});
Important: Use expect.assertions(n) to ensure your async assertions actually run!
Testing Callbacks with done
For callback-based APIs, use the done parameter:
function fetchDataCallback(callback) {
setTimeout(() => {
callback(null, { data: 'result' });
}, 100);
}
describe('callback testing', () => {
it('calls back with data', (done) => {
fetchDataCallback((error, data) => {
expect(error).toBeNull();
expect(data).toEqual({ data: 'result' });
done();
});
});
it('handles errors', (done) => {
fetchDataWithError((error, data) => {
expect(error).toBeInstanceOf(Error);
expect(data).toBeUndefined();
done();
});
});
});
Warning: If done() is never called, the test will timeout and fail.
Testing Timers
Jest can mock timers to avoid waiting:
function delayedGreeting(name, callback) {
setTimeout(() => {
callback(`Hello, ${name}!`);
}, 1000);
}
describe('timer testing', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('calls callback after delay', () => {
const callback = jest.fn();
delayedGreeting('Alice', callback);
// Callback not called yet
expect(callback).not.toHaveBeenCalled();
// Fast-forward time
jest.runAllTimers();
// Now it's been called
expect(callback).toHaveBeenCalledWith('Hello, Alice!');
});
it('respects the delay duration', () => {
const callback = jest.fn();
delayedGreeting('Bob', callback);
// Advance by 500ms
jest.advanceTimersByTime(500);
expect(callback).not.toHaveBeenCalled();
// Advance by another 500ms (total 1000ms)
jest.advanceTimersByTime(500);
expect(callback).toHaveBeenCalled();
});
});
Timer Methods
// Run all pending timers
jest.runAllTimers();
// Run only currently pending timers (not ones they schedule)
jest.runOnlyPendingTimers();
// Advance time by specific amount
jest.advanceTimersByTime(1000);
// Clear all timers
jest.clearAllTimers();
Testing setInterval
function createCounter(callback) {
let count = 0;
const id = setInterval(() => {
count++;
callback(count);
}, 1000);
return () => clearInterval(id);
}
describe('interval testing', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('increments count every second', () => {
const callback = jest.fn();
const stop = createCounter(callback);
jest.advanceTimersByTime(3000);
expect(callback).toHaveBeenCalledTimes(3);
expect(callback).toHaveBeenLastCalledWith(3);
stop();
});
});
Testing Multiple Async Operations
Use Promise.all or multiple awaits:
async function fetchUserWithPosts(userId) {
const [user, posts] = await Promise.all([
fetchUser(userId),
fetchPosts(userId)
]);
return { user, posts };
}
describe('parallel async operations', () => {
it('fetches user and posts together', async () => {
const result = await fetchUserWithPosts(1);
expect(result.user).toBeDefined();
expect(result.posts).toBeInstanceOf(Array);
expect(result.user.id).toBe(1);
});
});
Try It: Test Async Functions
Loading Code Editor...
Common Async Testing Mistakes
1. Forgetting to Return or Await
// Bad: Test passes before promise resolves
it('fetches data', () => {
fetchData().then(data => {
expect(data).toBeDefined(); // Never runs!
});
});
// Good: Return the promise
it('fetches data', () => {
return fetchData().then(data => {
expect(data).toBeDefined();
});
});
// Best: Use async/await
it('fetches data', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
2. Not Using expect.assertions
// Risk: If no error, test passes without running assertions
it('handles error', async () => {
try {
await mightFail();
} catch (e) {
expect(e.message).toBe('Error');
}
});
// Safe: Ensures assertion runs
it('handles error', async () => {
expect.assertions(1);
try {
await mightFail();
} catch (e) {
expect(e.message).toBe('Error');
}
});
3. Real Timers in Tests
// Bad: Test takes 5 seconds
it('waits for timeout', async () => {
await new Promise(r => setTimeout(r, 5000));
expect(something).toBe(true);
});
// Good: Use fake timers
it('waits for timeout', async () => {
jest.useFakeTimers();
const promise = waitFor(5000);
jest.advanceTimersByTime(5000);
await promise;
expect(something).toBe(true);
});
Key Takeaways
- Use
async/awaitfor clean, readable async tests - Use
.resolvesand.rejectsfor promise assertions - Use
donecallback for callback-based code - Use
jest.useFakeTimers()to avoid slow timer tests - Always return or await async operations
- Use
expect.assertions(n)to ensure assertions run - Mock timers with
jest.advanceTimersByTime()for precise control
Next, we'll learn about mocking - how to replace real implementations with test doubles!

