## TL;DR
Something asynchronous, a timer, an interval, a socket callback, or a floating promise, fired after jest tore the test down, and jest flags the late console output. CI scheduling makes it intermittent: locally the async work usually settles before teardown, in CI it does not. Find the open handle with --detectOpenHandles, then make the test await the async work, cancel it in teardown, or use fake timers so nothing outlives the test.

## The query
```text
jest Cannot log after tests are done flake in CI  -  how to find the async leak
```

## Use this when
- jest intermittently warns Cannot log after tests are done in CI
- the warning moves between test files from run to run
- --detectOpenHandles prints timers or sockets

## Not for
- console output during tests (normal and harmless)
- errors thrown after teardown rather than logs (related, but the fix is in error handling)
- the warning appears on every run deterministically (the leak is constant; still use these steps)

## Steps

### 1. Rerun with --detectOpenHandles
This flag makes jest print the handles keeping the process alive, which usually includes the leaking timer or socket.
```bash
npx jest --ci --detectOpenHandles src/services/poller.test.js 2>&1 | tail -40
```
Expected output: the output names the open handle type (Timeout, Socket, etc.) and often the creating call site.

### 2. Locate the source of the leaked async work
Search the test and the module under test for the handle type from step 1: setTimeout, setInterval, open sockets, or subscriptions without teardown.
```bash
grep -rn "setInterval\|setTimeout" src/services/poller.js
```
Expected output: the leaking call is found in test or production code.

### 3. Fix it: await, cancel, or fake the async work
Cancel timers in afterEach, close sockets, or switch the test to fake timers so nothing real outlives the test.
```js
afterEach(() => {
  jest.clearAllTimers();
});
test('polls once', () => {
  jest.useFakeTimers();
  // ... trigger and assert ...
  jest.useRealTimers();
});
```
Expected output: the test passes and the Cannot log warning is gone across repeated runs.

### 4. Make CI fail loudly on future leaks
Keep --detectOpenHandles in the CI invocation so the next leak fails the run instead of flaking it.
```bash
npx jest --ci --detectOpenHandles
```
Expected output: a future leak produces a clear failure with the handle printed, not a flake.

## Tool compatibility
- jest 26 - 30
- Node 16 - 22

## Variant phrasings
- Cannot log after tests are done flake in CI
- jest did not exit one second after the test run completed
- console.log called after test finished jest warning

## Why it happens
Jest tears down the test environment when the test function returns, but real timers and sockets live on the event loop, not in the test. When CI scheduling delays the callback past teardown, its console output lands in a dead test context and jest warns. The warning is a symptom of async work escaping the test boundary, and the fix is to bring that work back inside it.

## Edge cases / pitfalls
- third-party libraries with background timers (telemetry, polling SDKs) are frequent culprits; mock them in tests
- mixing the done callback with async/await can resolve the test before the async work finishes; pick one style
- jest.useFakeTimers() changes timer behavior for the whole file; scope it to the tests that need it
- an unhandled rejection from the leaked work can surface as a second, confusing failure; fix the leak and both go away

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_q-fJj_NmFX8QpckVz2h99Q
