Fix: Jest Async Test Timeout — Exceeded 5000ms or Test Never Resolves
Quick Answer
How to fix Jest async test timeouts — missing await, unresolved Promises, done callback misuse, global timeout configuration, fake timers, and async setup/teardown issues.
The Problem
A Jest test times out instead of failing or passing:
● UserService › fetchUser › returns user data
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.Or a test hangs indefinitely — Jest process never exits:
jest --testPathPattern=user.test.ts
# Tests pass but Jest process hangs — never returns to shell promptOr a Promise-based test always passes even when the assertion inside is false:
test('should reject with error', () => {
fetchUser(-1).catch(err => {
expect(err.message).toBe('User not found');
// This assertion never runs — test passes vacuously
});
// Promise returned to .catch but test doesn't wait for it
});Why This Happens
Jest’s async handling depends on the test function correctly signaling completion. Common failures:
- Missing
await— a Promise is created but not awaited. The test function returns immediately (before the async work finishes), and Jest marks it as passed without checking assertions. donecallback never called — using thedonepattern but not callingdone()(ordone(error)) in all code paths. Jest waits fordoneforever.- Unresolved Promise chain — a
.then()or.catch()that silently swallows errors leaves a Promise pending. Jest waits but the Promise never settles. - Open handles — a database connection, timer, or network socket opened during the test isn’t closed. Jest waits for all handles to close before exiting.
beforeAll/afterAllnot completing — if setup or teardown is async and doesn’t resolve, no tests in the suite run (they all timeout).- Default timeout too low — complex integration tests legitimately take more than 5 seconds. The global or per-test timeout needs increasing.
Fix 1: Add Missing await
The most common cause — async test functions without proper awaiting:
// WRONG — no async, Promise returned but not awaited
test('fetches user', () => {
fetchUser(1).then(user => {
expect(user.name).toBe('Alice'); // Assertion runs after test ends
});
// Test returns immediately — always "passes" (no assertions run)
});
// WRONG — async but missing await in the chain
test('fetches user', async () => {
const result = fetchUser(1); // Missing await — result is a Promise, not user
expect(result.name).toBe('Alice'); // Error: Cannot read .name of Promise
});
// CORRECT — await the Promise
test('fetches user', async () => {
const user = await fetchUser(1); // Waits for the Promise to resolve
expect(user.name).toBe('Alice'); // Runs after user is available
});
// CORRECT — return the Promise (works without async/await)
test('fetches user', () => {
return fetchUser(1).then(user => {
expect(user.name).toBe('Alice');
});
// Returning the Promise tells Jest to wait for it
});Common missing await locations:
// WRONG — beforeEach not awaited
beforeEach(() => {
db.connect(); // Missing await — tests run before DB connects
});
// CORRECT
beforeEach(async () => {
await db.connect();
});
// WRONG — assertion inside .then not returned
test('updates user', async () => {
await updateUser(1, { name: 'Bob' });
fetchUser(1).then(user => {
expect(user.name).toBe('Bob'); // Not returned — may not run
});
});
// CORRECT
test('updates user', async () => {
await updateUser(1, { name: 'Bob' });
const user = await fetchUser(1);
expect(user.name).toBe('Bob');
});Fix 2: Use done Callback Correctly
When using the done callback pattern, call it in ALL code paths:
// WRONG — done never called if fetchUser throws
test('fetches user', (done) => {
fetchUser(1).then(user => {
expect(user.name).toBe('Alice');
done();
});
// If fetchUser rejects, done is never called → timeout
});
// CORRECT — call done in error cases too
test('fetches user', (done) => {
fetchUser(1)
.then(user => {
expect(user.name).toBe('Alice');
done();
})
.catch(error => {
done(error); // Call done with error to fail the test cleanly
});
});
// BETTER — use async/await instead of done for clarity
test('fetches user', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});done with callbacks (not Promises):
// For callback-based APIs — done is appropriate
test('reads file', (done) => {
fs.readFile('./data.json', 'utf8', (err, data) => {
if (err) return done(err); // Fail test with error
const parsed = JSON.parse(data);
expect(parsed.version).toBe('1.0');
done(); // Signal completion
});
});Fix 3: Test Rejected Promises
Testing that a function rejects requires careful handling:
// WRONG — assertion in .catch not returned
test('rejects invalid user', () => {
fetchUser(-1).catch(err => {
expect(err.message).toBe('Not found');
// Not returned — test passes even if assertion fails
});
});
// CORRECT — use expect(...).rejects
test('rejects invalid user', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Not found');
// Or: await expect(fetchUser(-1)).rejects.toMatchObject({ message: 'Not found' });
});
// CORRECT — alternatively, handle manually
test('rejects invalid user', async () => {
expect.assertions(1); // Ensures at least 1 assertion runs
try {
await fetchUser(-1);
fail('Expected fetchUser to throw'); // Shouldn't reach here
} catch (err) {
expect(err.message).toBe('Not found');
}
});expect.assertions(n) — guard against missing assertions:
test('async operations all run', async () => {
expect.assertions(3); // Test fails if exactly 3 assertions don't run
const user = await fetchUser(1);
expect(user).toBeDefined(); // Assertion 1
expect(user.name).toBe('Alice'); // Assertion 2
expect(user.email).toContain('@'); // Assertion 3
// If any assertion is skipped (e.g., due to early return), test fails
});Fix 4: Increase Timeout for Slow Tests
When tests legitimately take longer than 5 seconds:
// Per-test timeout (milliseconds)
test('slow integration test', async () => {
const result = await slowDatabaseOperation();
expect(result).toBeDefined();
}, 30000); // 30 second timeout for this test
// Per-describe timeout (applies to all tests in the block)
describe('integration tests', () => {
beforeAll(async () => {
await startDatabase();
}, 30000); // 30s for beforeAll
test('complex query', async () => {
const data = await runComplexQuery();
expect(data.length).toBeGreaterThan(0);
}, 15000); // 15s for this test
});
// Global timeout — applies to all tests in the file
// jest.config.js
module.exports = {
testTimeout: 15000, // 15 seconds globally
};
// Or per-file using jest.setTimeout
// At the top of the test file:
jest.setTimeout(30000); // Applies to all tests in this fileFix 5: Fix Open Handles
Jest warns about open handles that prevent clean exit:
# Jest output:
# Jest did not exit one second after the test run has completed.
# This usually means that there are asynchronous operations that weren't stopped in your tests.
# Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
jest --detectOpenHandles
# Output shows which handles are open (database connections, timers, servers)Common open handles and how to fix them:
// Database connection not closed
describe('UserService', () => {
let connection;
beforeAll(async () => {
connection = await createDatabaseConnection();
});
afterAll(async () => {
await connection.close(); // MUST close after tests
});
test('finds user', async () => {
const user = await connection.query('SELECT * FROM users WHERE id = 1');
expect(user).toBeDefined();
});
});
// HTTP server not closed
let server;
beforeAll(done => {
server = app.listen(3001, done);
});
afterAll(done => {
server.close(done); // Close server after tests
});
// setInterval or setTimeout not cleared
let intervalId;
beforeAll(() => {
intervalId = setInterval(pollService, 1000);
});
afterAll(() => {
clearInterval(intervalId); // Stop the interval
});Force Jest to exit after tests complete:
# Force exit after tests (workaround — fix the actual handle instead)
jest --forceExit
# Or in jest.config.js:
module.exports = {
forceExit: true, // Not recommended — hides the real issue
};Fix 6: Fix async beforeAll and afterAll
Setup and teardown must properly resolve for tests to run:
// WRONG — async beforeAll not awaited correctly
beforeAll(() => {
setupDatabase(); // Returns Promise but not returned
// Jest doesn't wait — tests run before DB is set up
});
// CORRECT — return the Promise
beforeAll(() => {
return setupDatabase(); // Jest waits for this Promise
});
// CORRECT — async/await
beforeAll(async () => {
await setupDatabase();
await seedTestData();
await startMockServer();
});
// CORRECT — done callback
beforeAll(done => {
setupDatabase()
.then(() => done())
.catch(done);
});
// afterAll — must clean up or remaining tests may timeout
afterAll(async () => {
await db.clearTestData();
await db.disconnect();
mockServer.stop();
});Fix 7: Fake Timers with Async Tests
Combining jest.useFakeTimers() with async tests requires special handling:
// WRONG — fake timers prevent Promise resolution in some setups
jest.useFakeTimers();
test('debounced function calls', async () => {
const fn = jest.fn();
const debouncedFn = debounce(fn, 500);
debouncedFn();
jest.advanceTimersByTime(500);
// If your async code uses setTimeout internally,
// Promises may not resolve with fake timers
await waitFor(() => expect(fn).toHaveBeenCalled());
// Can hang if waitFor uses real time intervals
});
// CORRECT — use runAllTimers or specific advancement
jest.useFakeTimers();
test('debounced function calls', () => {
const fn = jest.fn();
const debouncedFn = debounce(fn, 500);
debouncedFn();
jest.runAllTimers(); // Executes all pending timers immediately
expect(fn).toHaveBeenCalledTimes(1);
});
// For tests mixing real Promises and fake timers:
test('async with fake timers', async () => {
const result = someAsyncOperation(); // Returns Promise
// Advance timers to unblock any setTimeout inside someAsyncOperation
jest.runAllTimersAsync(); // Jest 29+ — runs timers and flushes Promises
expect(await result).toBeDefined();
});Still Not Working?
jest --runInBand — run tests serially (one at a time) to isolate which test is timing out:
jest --runInBand --testPathPattern=problem.test.tsCircular Promises — a Promise that depends on itself or creates a cycle never resolves. Add console.log statements at key points to trace where execution stops.
Module mocking hiding async errors — if a mocked module’s async function resolves immediately but the real implementation has async delays, the test may work in unit tests but fail in integration. Verify that mocks accurately represent real async timing.
--testTimeout=60000 flag — use a very high timeout temporarily to confirm the test can pass (just slowly), vs never passing at all:
jest --testTimeout=60000 problem.test.tsFor related testing issues, see Fix: Jest Fake Timers Not Working and Fix: Jest Coverage Not Collected.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Jest Setup File Not Working — setupFilesAfterFramework Not Running or Globals Not Applied
How to fix Jest setup file issues — setupFilesAfterFramework vs setupFiles, global mocks not applying, @testing-library/jest-dom matchers, module mocking in setup, and TypeScript setup files.
Fix: Jest Coverage Not Collected — Files Missing from Coverage Report
How to fix Jest coverage not collecting all files — collectCoverageFrom config, coverage thresholds, Istanbul ignore comments, ts-jest setup, and Babel transform issues.
Fix: Jest Fake Timers Not Working — setTimeout and setInterval Not Advancing
How to fix Jest fake timers not working — useFakeTimers setup, runAllTimers vs advanceTimersByTime, async timers, React testing with act(), and common timer test mistakes.
Fix: Jest Module Mock Not Working — jest.mock() Has No Effect
How to fix Jest module mocks not working — hoisting behavior, ES module mocks, factory functions, mockReturnValue vs implementation, and clearing mocks between tests.