Jest did not exit — Find and Fix Open Handles
Diagnose Jest hanging after tests by running detectOpenHandles, identifying servers, timers, and database clients left open, then closing them in afterAll hooks.
Symptoms
- Jest did not exit one second after the test run has completed
- CI jobs hang after all tests pass until a timeout kills them
- The process exits only when --forceExit is used
- The warning lists open handles from TCP servers or timers
Possible causes
- An HTTP server started in tests is never closed
- Database, Redis, or message broker clients keep sockets open
- setInterval or setTimeout timers are created and not cleared
- A library starts a background worker or watcher during import
Diagnosis steps
Run with open handle detection
npx jest --detectOpenHandles --runInBandInterpretation
The report lists each open handle with a stack trace showing where it was created. --runInBand avoids worker processes hiding the origin.
Identify the owning test file
npx jest --detectOpenHandles --runInBand 2>&1 | grep -B2 -A12 'Jest has detected the following' | head -80Interpretation
The trace points to the module that created the handle. A server.close or client.disconnect missing in afterAll is the usual finding.
Confirm it is not just a slow test
npx jest --runInBand --verbose --testTimeout=10000 2>&1 | tail -30Interpretation
If the suite reports all tests passed and then waits, it is a handle leak. If a test is still running, the timeout is the issue instead.
Use forceExit only to confirm the diagnosis
npx jest --forceExit --runInBandInterpretation
A clean exit with --forceExit proves Jest is waiting on handles rather than doing work. It does not fix the leak.
Root cause
Jest waits for the Node event loop to drain before exiting. Any active TCP server, database connection, interval timer, or file watcher keeps the loop alive, so the run appears complete while the process never terminates. Libraries that create clients at import time can leak even when the test body looks clean.
Fix
Close every resource in afterAll or afterEach: await new Promise(resolve => server.close(resolve)), call client.disconnect() or pool.end(), clear timers, and stop workers. Create clients inside beforeAll rather than at module scope, and mock modules that open connections by default. Reserve --forceExit for a temporary CI unblock.
Risk note
ForceExit can truncate asynchronous cleanup and hide real failures, and it may drop coverage writes. Closing a shared connection too early breaks later tests. Fake timers change promise scheduling and can make unrelated tests flaky if not restored.
Verify the fix
Run the full suite with --detectOpenHandles and confirm no handles are reported, then run without --forceExit and verify the process exits with code 0 on its own. Repeat in CI with the same worker count used in production pipelines.
Passing tests, hanging process
Jest exits when the event loop is empty. A test suite can report every test passed and still hang because a server, socket, or interval is keeping Node alive:
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.
Locate the handle
npx jest --detectOpenHandles --runInBand
--runInBand keeps everything in one process so the stack trace points at the real creator. The report usually names a TCP server, a Timeout from setInterval, or a client from a database driver.
Close resources deterministically
let server;
let pool;
beforeAll(async () => {
pool = await createPool();
server = app.listen(0);
});
afterAll(async () => {
await new Promise((resolve) => server.close(resolve));
await pool.end();
});
Move client creation out of module scope, unref or clear timers, and mock modules that connect on import. If a dependency starts a background worker, call its shutdown method or isolate it behind a mock.
Verify
Run the suite without --forceExit and confirm the process returns to the shell with exit code 0. Then run with --detectOpenHandles and check the report is empty. Reproduce in CI with the same worker settings; a leak that only appears with several workers usually means a per-worker resource is not closed.