What this usually means
Jest's test runner waits for asynchronous operations to complete. If a test uses a callback-based async pattern (like done) or returns a promise, Jest waits. When that promise never resolves, or done is never called, Jest eventually terminates the test with a timeout error. The default timeout is 5000ms. Common causes include unhandled promise rejections, incorrect mock implementations that don't resolve, missing done() in callback tests, or actual slow operations that need a higher timeout. Sometimes the issue is that an async function is declared but not awaited, leaving a dangling promise.
The first ten minutes — establish facts before touching code.
- 1Identify the exact test: run `npx jest --testNamePattern='test name' --verbose` to isolate the failing test and see its timeout error.
- 2Check if the test returns a promise: look for `async` keyword in the test function or a `return` statement with a promise. If using `done` callback, ensure it's called in every code path.
- 3Add a `.catch()` to any promise chain inside the test and log to see if a rejection is unhandled: `promise.catch(e => console.log('rejected:', e))`
- 4Set `jest.setTimeout(10000)` at the top of the test file and re-run to see if the test completes with a longer timeout—if yes, the issue is performance, not a hang.
- 5Run the test with `--detectOpenHandles` flag: `npx jest --detectOpenHandles` to identify handles (like open connections or timers) preventing Jest from exiting.
- 6Add a `console.log` before and after the async operation to see if the code reaches the end of the test.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe test file itself, especially the test function body and any `done` callback usage.
- searchMock implementations: check if mocked functions return a promise that never resolves. Look for `mockReturnValue` vs `mockResolvedValue`.
- search`jest.setTimeout()` calls in the test file or setup files to see if timeouts are overridden.
- searchGlobal setup/teardown files (setupFilesAfterFramework) that might introduce async operations that don't complete.
- searchThe `beforeEach`/`afterEach` blocks: if they perform async operations without returning a promise or calling `done`, they can block tests.
- searchNetwork requests or timers in the code under test: ensure they are mocked or handled properly.
- searchJest configuration in `package.json` or `jest.config.js` for `testTimeout` setting.
Practical causes, not theory. These are the things you will actually find.
- warningMissing `done()` call in callback-based tests, or `done` is called inside a conditional branch that doesn't execute.
- warningReturning a promise that never resolves because of an unhandled rejection or a never-settled mock.
- warningAsync test function but no `await` for the async operation—the test returns immediately, but the operation runs and may cause side effects later.
- warningIncorrect mock: using `mockReturnValue(Promise.resolve())` instead of `mockResolvedValue()`—the promise is not properly chained.
- warningGlobal timeout overridden by a lower value in a specific test file or by a test runner option.
- warningTimers (setTimeout, setInterval) that are not cleared, keeping the event loop busy and preventing Jest from timing out the test properly.
- warningThird-party library integration that opens persistent connections (e.g., WebSocket, database) not closed in afterAll.
Concrete fix directions. Pick the one that matches your root cause.
- buildEnsure every test that uses `done` calls it exactly once, including in error callbacks: wrap `done` in a try-catch or use `.finally`.
- buildConvert callback-based tests to async/await: replace `done` with `async` function and `await` the async operation.
- buildUse `mockResolvedValue` or `mockRejectedValue` for mocks that return promises, not `mockReturnValue`.
- buildIncrease the test timeout for slow operations: call `jest.setTimeout(10000)` inside the test or set globally in config.
- buildUse `expect.assertions(n)` to ensure a certain number of assertions are called, which helps catch missing `done` calls.
- buildClear timers and close connections in `afterEach` or `afterAll` using `clearInterval`, `clearTimeout`, or mock cleanup.
- buildRun tests with `--forceExit` as a last resort—this forces Jest to exit after all tests complete, but masks underlying issues.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun the specific test with increased timeout to confirm it passes: `jest --testNamePattern='test' --testTimeout=10000`
- verifiedRun the full test suite with `--detectOpenHandles` to ensure no open handles remain.
- verifiedAdd `expect.hasAssertions()` at the top of the test and verify the test fails if no assertion runs (helps detect missing done).
- verifiedTemporarily replace async operations with `Promise.resolve()` to isolate the async part as the cause.
- verifiedRun tests with `--verbose` and check the time each test takes to see if any is near the timeout limit.
- verifiedUse `jest.useFakeTimers()` to mock timers and manually advance time to avoid real async delays.
Things that make this bug worse or harder to find.
- warningNever use `done` with `async` functions—Jest will wait indefinitely if both are used.
- warningAvoid setting `jest.setTimeout(0)` which disables timeout but can cause infinite hangs.
- warningDon't rely on `--forceExit` as a permanent fix; it can hide resource leaks.
- warningDon't ignore unhandled promise rejections—they often indicate a missing catch that leads to timeout.
- warningAvoid using `setTimeout` with a callback inside tests without clearing it or using fake timers.
- warningDon't put `return` before an async call that you intend to await; it will return a promise that might be lost.
CI Pipeline Fails After Upgrade from Jest 26 to 28: Async Timeout in Integration Test
Timeline
- 09:15PR opened upgrading Jest 26→28. CI runs but fails on one integration test with timeout after 5000ms.
- 09:20Developer runs the test locally—passes. CI consistently fails.
- 09:30Check CI config: no difference in test script or timeout settings.
- 09:45Add `console.log` before and after async API call in test. CI logs show log before but not after.
- 10:00Suspect MongoDB memory server startup is slow in CI. Check `beforeAll`—it uses `mongodb-memory-server` with default download timeout.
- 10:10Check `jest.setTimeout` in test file: not set. Default is 5000ms. MongoDB download can take >10s on first run in CI.
- 10:15Add `jest.setTimeout(30000)` in `beforeAll`. Test passes in CI.
- 10:20Also add `mongod.stop()` in `afterAll` to ensure cleanup.
We were upgrading Jest from 26 to 28 across our microservices. One integration test that used supertest with an in-memory MongoDB started failing only in CI. The error was the classic 'Timeout - Async callback was not invoked within the 5000 ms timeout'. I pulled the branch, ran the test locally—green. That was suspicious. CI always runs on clean containers, so any startup cost hits hard.
I added console.logs around the async API call inside the test. The log before the call appeared, but the log after never did. That told me the promise from the API call was hanging. The API itself was a simple Express route that queried MongoDB. In CI, the MongoDB memory server had to download the binary on first run because the container was fresh. That download could take 10-15 seconds, but the test timeout was still the default 5000ms.
The fix was straightforward: increase the timeout for that test file using `jest.setTimeout(30000)` at the top. I also verified that the `afterAll` properly stopped the MongoDB server to avoid open handles. After that, the test passed consistently. The lesson: always consider environment differences, especially first-run costs for external services. Also, never assume a test that passes locally is correct—CI is the truth.
Root cause
MongoDB memory server binary download on first CI run exceeded the default 5000ms test timeout.
The fix
Added `jest.setTimeout(30000)` at the top of the test file to accommodate slow startup, and ensured proper cleanup with `afterAll`.
The lesson
Always set explicit timeouts for integration tests that involve external resource setup, especially in ephemeral CI environments.
Jest categorizes async tests into three patterns: callback-based using `done`, promise-based by returning a promise, and async/await (which is syntactic sugar for promise-based). When Jest sees a test function that accepts a `done` parameter or returns a promise, it waits for that async work to complete. The timeout counter starts when the test function begins execution. If the promise never settles or `done` is never called, Jest will throw a timeout error after the configured threshold.
The default timeout is 5000ms, set globally by Jest. You can override it per-test with `jest.setTimeout(ms)` or globally in the config with `testTimeout`. It's important to note that `jest.setTimeout` must be called inside a test or hook (like `beforeAll`) and sets the timeout for that specific scope. A common mistake is calling it at the top level of a module, which doesn't work because the test runner may not have started yet.
The `done` callback is a frequent source of timeouts. You must call `done()` exactly once, and in every code path including error handlers. For example, if you have a promise and call `done` in the `.then` but forget it in the `.catch`, the test will hang on rejection. A robust pattern is to wrap the entire async body in a try-catch and call `done` with the error if caught, or use `.finally` to ensure `done` is called.
Another pitfall is using `done` with an `async` test function. Jest will wait for the returned promise AND the `done` callback, leading to a double completion or a timeout if one is missing. My rule: never mix `done` with `async`. If you need async, simply return the promise or use `await`. If you must use callbacks, use a non-async function and rely on `done`.
Jest's `--detectOpenHandles` flag is a powerful tool for diagnosing timeouts caused by resources that keep the event loop alive. When you run `npx jest --detectOpenHandles`, Jest will print warnings about handles that are still open after the test suite finishes. These can include network connections, timers, file handles, or database connections. If a test timeout is caused by a resource leak, the error might not point to the actual leak—the `--detectOpenHandles` output will.
For example, if you have a `setInterval` that is not cleared in `afterEach`, the event loop stays active and Jest may hang or timeout. The fix is to clear all timers and close connections in teardown hooks. In modern Jest, using `jest.useFakeTimers()` can also help by replacing real timers with mocks that can be controlled.
A classic scenario is a test that passes locally but times out in CI. The root cause is usually environmental: CI machines are slower, have less memory, or have network latency. For example, downloading a Docker image, starting a database, or hitting an external API can take much longer in CI. The solution is to set an appropriate timeout for the entire test suite or per-test file that accounts for the worst-case CI environment.
You can set a global timeout in `jest.config.js` with `testTimeout: 30000`. However, be careful not to set it too high globally, as it can mask real hangs. A better approach is to set timeouts selectively for slow tests using `jest.setTimeout` inside the test or hook. Also, consider optimizing the test setup: pre-download binaries, use fixtures, or mock external services to reduce startup time.
When your code relies on timers (`setTimeout`, `setInterval`) or dates, real async delays can cause timeouts. Jest provides `jest.useFakeTimers()` to replace the native timer functions with mocks. This allows you to manually advance time using `jest.advanceTimersByTime(ms)` or `jest.runAllTimers()`. This makes tests deterministic and fast, eliminating timeout issues caused by waiting for real time.
However, fake timers can be tricky with promises. If your code uses promises that resolve asynchronously, you may need to flush promises using `jest.useFakeTimers({ legacyFakeTimers: false })` and `jest.advanceTimersByTime` combined with `await Promise.resolve()`. Also, ensure you call `jest.useRealTimers()` after the test to restore native timers, especially if you have other tests that rely on real timing.
Frequently asked questions
Why does my Jest test timeout even though I'm using async/await?
The most common reason is that the async function is not awaited properly. For example, if you call an async function without `await` inside the test, Jest may consider the test complete before the async operation finishes, but if the returned promise is not handled, it might still cause a timeout if the operation hangs. Also, check that the test function itself is `async`. Another cause is that the async operation never resolves—maybe because it's waiting for an event that never fires, or a mock doesn't resolve.
How do I increase the timeout for a single Jest test?
Use `jest.setTimeout(milliseconds)` inside the test function or in a `beforeEach`/`beforeAll` hook that applies to that test. For example: `test('my test', () => { jest.setTimeout(10000); // rest of test });`. You can also set a global timeout in the Jest config under `testTimeout`. Note that `jest.setTimeout` sets the timeout for the current test and any hooks within its scope.
What's the difference between `mockReturnValue(Promise.resolve())` and `mockResolvedValue()`?
`mockResolvedValue(value)` is shorthand for `mockReturnValue(Promise.resolve(value))`. However, `mockReturnValue` returns a value synchronously, while `mockResolvedValue` returns a promise that resolves asynchronously. If you use `mockReturnValue` with a promise object, the mock will return that promise directly, which may not behave correctly if the code expects the promise to be awaited properly. In practice, always prefer `mockResolvedValue` for async mocks to ensure the mock behaves as an async function.
Can a `beforeEach` hook cause a test timeout?
Yes, if `beforeEach` performs an async operation and does not return a promise or call `done`, Jest will wait for it to complete. If that operation hangs or takes longer than the test timeout, the test will fail with a timeout error. Always ensure that async hooks either return a promise or use the `done` callback properly. Also, note that the timeout for hooks is the same as the test timeout; you can set a separate timeout inside the hook using `jest.setTimeout`.
What does 'Exceeded timeout of 5000 ms for a test' mean when I don't have any async code?
It means Jest thinks there is async code. This can happen if you inadvertently use `async` in the test function but don't actually have any async operations. The async function returns a promise, and Jest waits for it. If the promise never resolves (which should be immediate for a synchronous async function), but often the issue is that the test function itself is not properly returning. Another possibility is that you are using `done` inadvertently, e.g., the test function has a parameter named `done`. Check your test signature: if it has any parameter, Jest assumes it's a callback-based test.