Fix: Jest Timeout — Exceeded timeout of 5000ms for a test
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Jest 'Exceeded timeout of 5000ms for a test' errors caused by unresolved promises, missing done callbacks, async/await mistakes, and slow database or network calls in tests.
What Jest Just Told You
Personally, of every CI failure I have triaged in the past two years on my team, Jest timeouts are tied for first place with flaky network tests. I tried to reproduce a few of them locally and could never get them to fail on my laptop. The error itself is unambiguous; the root cause almost never is. You run Jest and one or more tests fail with:
Thrown: "Exceeded timeout of 5000ms for a test.
Add a timeout value to this test to increase the timeout, if this is a long-running test;
see https://jestjs.io/docs/api#testname-fn-timeout."Or:
● fetchUser › returns user data
Exceeded timeout of 5000ms for a test.
at node_modules/jest-jasmine2/build/queueRunner.js:47:12The test starts, runs for 5 seconds (Jest’s default timeout), then fails — even if the code itself would eventually succeed given more time.
How Jest’s Timeout Mechanism Works
Jest uses a 5000ms (5 second) default timeout per test. If a test does not complete within that window, Jest forcibly fails it and moves on. The timeout exists because a hung test would otherwise block the entire suite indefinitely — CI in particular would hang for an hour or more before the runner killed the job. Five seconds is short enough to surface real problems quickly and long enough to accommodate genuinely slow legitimate work.
A test “completing” means Jest’s test runner has observed the end of the test function. That signal arrives in one of three ways: the synchronous function returned, a promise returned by the function settled, or the done callback was invoked. If none of those happens within the configured timeout, Jest fails with the message you see. The key insight is that Jest does not introspect what your code is doing — it only watches those three signals. A test that quietly forgets to return a promise looks identical to a test where the promise is genuinely hung.
The five-second budget covers everything: any beforeAll and beforeEach hooks, the test body, all await points, all network calls, and any work scheduled on timers that the test waits for. On constrained CI runners (GitHub Actions free tier with 2 vCPUs, Vercel build sandboxes, Docker containers with CPU limits) tests that pass on a developer laptop frequently fail in CI for no reason other than that the runner is slower. The fix is rarely to raise the timeout — it is usually to mock the slow dependency.
Common causes:
- A Promise never resolves or rejects — a missing
resolve()/reject()call, or a callback that never fires. donecallback never called — using the callback-style async pattern but forgetting to calldone().awaiton a function that never settles — an infinite loop, deadlock, or event that never fires.- Actual network or database calls in tests without mocking — real I/O is slow and flaky.
beforeAllorbeforeEachtiming out — setup hooks count against the timeout too.- Missing
returnon a promise inside a test — Jest does not know to wait for it.
Platform and Environment Differences
The same test code can pass on your machine and fail under Jest’s timeout elsewhere. These environment-specific quirks account for most of the surprises.
Node.js version matters more than people expect. Node 18 enabled fetch and Headers globally without a polyfill, and the default keep-alive behavior of the global fetch keeps a connection pool open for a short window. Tests that hit localhost may run faster on Node 18+ than on Node 16 because of this. Tests that use node-fetch or axios on Node 16 can also time out on slow CI hosts that take longer to resolve DNS — Node 18’s fetch uses undici with different DNS caching.
Jest version differences in fake timers. Jest 26 used lolex, Jest 27 switched to @sinonjs/fake-timers and introduced modern as the default. Code that calls jest.useFakeTimers('legacy') works on Jest 27 and 28 but throws on Jest 29 unless you explicitly opt in. Tests that worked silently under legacy fake timers can hang under modern fake timers because setImmediate, process.nextTick, and microtasks are now under the timer’s control. If a test times out only after a Jest upgrade, check whether jest.advanceTimersByTimeAsync is needed instead of jest.advanceTimersByTime.
jest-environment-jsdom vs jest-environment-node. JSDOM is significantly slower than the node environment — a clean JSDOM instance takes 200–500ms to set up on a typical laptop. Tests that do not need DOM access should declare testEnvironment: 'node' in jest.config.js, or use /** @jest-environment node */ per file. Suites of 200+ tests can save tens of seconds and avoid spurious timeouts on slow CI hosts by avoiding JSDOM where possible.
GitHub Actions runner CPU constraints. The free ubuntu-latest runner gives you 2 vCPUs and 7 GB RAM. Tests that use Promise.all to parallelize heavy work hit CPU contention and run roughly half as fast as on a 4+ core developer machine. The right adjustment is to make tests use real time less aggressively — mock more, or use --maxWorkers=2 to prevent Jest from oversubscribing the runner.
Docker container resource limits. Tests running inside docker run --cpus 1 or under Kubernetes pods with low CPU requests behave like the GitHub Actions case but worse. Inside Docker Desktop on macOS, the Linux VM also adds latency to filesystem operations, which slows tests that read many files (snapshot-heavy suites, especially). Always run CI tests at least once inside the same container image used in production CI to catch these.
macOS file watcher overhead. Jest’s --watch mode uses fsevents on macOS, which is fast, but on Linux it falls back to polling unless chokidar finds inotify capacity. Inside an Apple Silicon Mac running Rosetta-translated Node, watch mode adds noticeable startup time per test run and can cause beforeAll hooks to time out on the first execution while caches warm up. Use Node built natively for arm64 to avoid this.
WSL2 filesystem performance. Reading and writing files across the Windows/WSL boundary (/mnt/c/...) is an order of magnitude slower than working entirely within the WSL2 ext4 disk. Tests that load fixtures from /mnt/c time out on WSL2 even though they run instantly on native Linux. Move the project into the WSL filesystem (e.g., ~/project) for normal Jest performance.
Fix 1: Return or Await the Promise
The most common mistake: the test completes synchronously, then the promise settles after Jest has already moved on — or Jest doesn’t wait at all.
Broken — promise not returned:
test("fetches user data", () => {
fetchUser(1).then(user => {
expect(user.name).toBe("Alice"); // Jest doesn't wait for this
});
// Test completes immediately — no assertion actually runs
});Fixed — return the promise:
test("fetches user data", () => {
return fetchUser(1).then(user => {
expect(user.name).toBe("Alice");
});
});Fixed — use async/await:
test("fetches user data", async () => {
const user = await fetchUser(1);
expect(user.name).toBe("Alice");
});async/await is the clearest pattern. Jest detects that the test function is async and waits for it to resolve before marking the test as done.
Fix 2: Fix the done Callback Pattern
When using the done callback (older pattern), Jest waits until done() is called. If it is never called — due to an error or a missing branch — the test times out.
Broken — done not called on error:
test("sends notification", (done) => {
sendNotification("[email protected]", (err, result) => {
if (err) {
// Forgot to call done() or done(err)
console.error(err);
return;
}
expect(result.sent).toBe(true);
done();
});
});Fixed — always call done:
test("sends notification", (done) => {
sendNotification("[email protected]", (err, result) => {
if (err) {
done(err); // Pass the error to done — fails the test with the actual error
return;
}
expect(result.sent).toBe(true);
done();
});
});Calling done(err) with an error argument fails the test immediately with a descriptive error instead of timing out.
I have not written a done-callback test in years. The pattern is a relic from before Jest supported returning promises, and the “forgot to call done” failure mode is among the most frustrating to diagnose because the test “passes” by timeout rather than by assertion. If you find one in a codebase, the cheapest fix is usually to convert it to async/await.
Fix 3: Mock Network and Database Calls
Tests that make real HTTP requests or database queries are slow, flaky, and environment-dependent. Mock them instead:
Broken — real HTTP call:
test("loads posts from API", async () => {
const posts = await fetch("https://jsonplaceholder.typicode.com/posts")
.then(r => r.json());
expect(posts.length).toBeGreaterThan(0);
// Fails if the network is slow, the API is down, or the test runs in CI
});Fixed — mock with jest.fn() or MSW:
// Using jest.spyOn to mock fetch
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => [{ id: 1, title: "Post 1" }, { id: 2, title: "Post 2" }],
});
test("loads posts from API", async () => {
const posts = await loadPosts();
expect(posts.length).toBe(2);
expect(global.fetch).toHaveBeenCalledWith("/api/posts");
});Using jest.mock() for modules:
jest.mock("./api", () => ({
fetchUser: jest.fn().mockResolvedValue({ id: 1, name: "Alice" }),
}));
import { fetchUser } from "./api";
test("fetchUser returns user", async () => {
const user = await fetchUser(1);
expect(user.name).toBe("Alice");
});For database calls, use an in-memory database (SQLite with :memory:, mongodb-memory-server for MongoDB) or mock the database layer entirely.
Fix 4: Increase the Timeout for Slow Tests
If the test is legitimately slow (integration test, large file processing, real database), increase the timeout:
Per-test timeout:
test("processes large CSV file", async () => {
const result = await processCSV("large-file.csv");
expect(result.rows).toBe(100000);
}, 30000); // 30 second timeout for this test onlyPer-suite timeout in beforeAll:
describe("database integration tests", () => {
beforeAll(async () => {
await setupDatabase();
}, 60000); // 60 seconds for setup
test("queries users table", async () => {
const users = await db.query("SELECT * FROM users");
expect(users.length).toBeGreaterThan(0);
}, 15000); // 15 seconds per test
});Global timeout for all tests:
In jest.config.js:
module.exports = {
testTimeout: 15000, // 15 seconds for all tests
};Or in jest.config.ts:
import type { Config } from "jest";
const config: Config = {
testTimeout: 15000,
};
export default config;I once inherited a codebase with jest.setTimeout(120000) in the setup file. The CI took 40 minutes on a green run. We deleted the line, found 18 tests that needed real fixes, and the suite dropped to four minutes. Raising the global timeout to “fix” failures hides real bugs and burns CI time on every run. Increase the timeout only for tests that are genuinely slow by design.
Fix 5: Fix beforeAll and afterAll Timeouts
Setup and teardown hooks have their own timeout (same as the test timeout by default). Database connections and server startup can be slow:
Broken — beforeAll times out:
beforeAll(async () => {
await startServer(); // Takes 8 seconds — times out at 5
await seedDatabase();
});Fixed — pass timeout as second argument:
beforeAll(async () => {
await startServer();
await seedDatabase();
}, 30000); // 30 seconds for setup
afterAll(async () => {
await stopServer();
await clearDatabase();
}, 15000);Fix 6: Fix Promises That Never Settle
If a promise hangs indefinitely, no timeout increase will fix it — find why it never resolves:
Common causes:
// Broken — resolve/reject never called
const wait = () => new Promise((resolve, reject) => {
someEmitter.on("done", () => {
// resolve() is missing — promise hangs forever
console.log("done");
});
});
// Broken — awaiting a non-promise
async function test() {
await undefined; // Immediately resolves, but if you await a never-resolving thing:
await new Promise(() => {}); // Hangs forever
}Diagnose with a race and timeout:
const withTimeout = (promise, ms) =>
Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
),
]);
test("operation completes", async () => {
const result = await withTimeout(myOperation(), 3000);
expect(result).toBe("done");
});This gives you a descriptive error (“Timed out after 3000ms”) instead of Jest’s generic timeout message, making it easier to see which operation hangs.
Fix 7: Fix Open Handles Warning
After fixing timeouts, Jest may warn:
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.Open handles (server sockets, database connections, timers) prevent Jest from exiting cleanly. Find them:
npx jest --detectOpenHandlesThen close them in afterAll:
let server;
beforeAll(async () => {
server = app.listen(3001);
});
afterAll(async () => {
await new Promise(resolve => server.close(resolve)); // Close the server
await db.end(); // Close database connections
});For timer-based open handles, use Jest’s fake timers:
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});Stranger Timeout Causes I Have Hit
Check for circular async dependencies. If function A awaits function B, and B awaits A, both hang indefinitely. Add logging inside each function to trace where execution stops.
Check event listeners that never fire. If a promise resolves only when an event is emitted, and that event never fires (e.g., a stream that never closes), the promise hangs. Add a timeout or verify the event source is working.
Run the specific failing test in isolation:
npx jest --testPathPattern="your-test-file" --verboseIsolating the test removes interference from other tests and makes the hang easier to reproduce.
Check for missing jest.config.js or package.json jest config. If Jest is picking up the wrong configuration, the testTimeout setting may not apply. Run npx jest --showConfig to see the resolved configuration.
Check for fake timer leakage between tests. If one test calls jest.useFakeTimers() but does not restore real timers in afterEach, subsequent tests inherit fake timers and any await on a real promise (a real network call, a real setTimeout) hangs forever because the clock never advances. Always pair useFakeTimers with useRealTimers in afterEach, and prefer jest.useFakeTimers({ doNotFake: ['nextTick', 'setImmediate'] }) if microtasks are causing trouble. See Fix: Jest Fake Timers Not Working for related issues.
Check process.exit calls inside the tested code. If production code calls process.exit() defensively on errors, Jest’s child worker process dies mid-test and the test never reports completion — eventually Jest’s outer runner declares a timeout. Mock process.exit in your setup file or refactor the code to throw instead of exit. See Fix: Node Uncaught Exception for related debugging.
Check for hung database transactions. A beforeEach that opens a transaction and never commits leaves PostgreSQL or MySQL row locks held. The next test waits for the lock and times out. Always wrap test transactions in try/finally with explicit ROLLBACK. For ESM-related issues that surface as timeouts during module load, see Fix: Jest ESM Error.
Check --detectOpenHandles output carefully. If Jest reports open handles for TCPSERVERWRAP or Timeout, the real fix is closing them in afterAll — but the open handle itself may be why your test timed out (a server.listen that never returned because the port was already bound). For module resolution timeouts at startup, see Fix: Jest Cannot Find Module.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Node.js Test Runner Not Working — node --test, TypeScript, Mocks, Coverage, and Watch Mode
How to fix Node.js built-in test runner errors — node --test not finding files, ESM vs CJS imports, TypeScript with --experimental-strip-types, mock.method isolation, coverage reporting, and watch mode setup.
Fix: Jest Cannot Transform ES Modules — SyntaxError: Cannot use import statement
How to fix Jest failing with 'Cannot use import statement outside a module' — configuring Babel transforms, using experimental VM modules, migrating to Vitest, and handling ESM-only packages.
Fix: Node.js Stream pipeline() Not Working — Backpressure, Error Propagation, AbortSignal, and Web Streams Interop
How to fix Node.js stream/promises pipeline errors — uncaught stream errors, backpressure ignored, AbortSignal not propagating, async iterators in pipeline, Transform stream object mode, and converting between Node and Web Streams.
Fix: Bun Test Not Working — Module Mocking, DOM Setup, Coverage, and Watch Mode
How to fix Bun test runner issues — mock.module not isolating, happy-dom setup for DOM tests, --coverage missing files, timer mocks, snapshot updates, TypeScript path aliases, and preload files.