jest-doctor

main codecov npm version License

jest-doctor is a custom Jest environment that detects async leaks between tests and fails flaky tests deterministically. It enforces strong test isolation and hygiene by checking for unresolved promises, open timers, and other side effects at test boundaries.

If your Jest tests sometimes fail only in CI or only when run together, async leaks are often the cause β€” and jest-doctor is designed to catch them reliably.

πŸš€ Quick Start

npm install --save-dev jest-doctor

or

yarn add -D jest-doctor

Add one of the provided environments to your jest.config.js. Out-of-the-box jest-doctor supports node and jsdom environments. But you can also build your own environment.

export default {
  testEnvironment: 'jest-doctor/env/node',
  // optional
  reporters: ['default', 'jest-doctor/reporter'],
};

After running tests, a report like this is shown for each detected leak:

report promise leak

πŸ” What problems does it catch?

jest-doctor detects common causes of flaky Jest tests by checking that each test fully cleans up its async work and side effects before the next test runs.

It detects and reports when tests that:

Why Jest’s --detectOpenHandles is not enough to prevent flaky tests

Jest already offers a built-in solution to detect open handles. But it often does not report any issues and will not provide actionable advice. The motivation page goes into more detail.

How jest-doctor works

For a more detailed explanation, see the architecture section.

βš™οΈ Configuration

The environment can be configured through the Jest config testEnvironmentOptions.

List of all available options:

A detailed description of the configuration options can be found at configuration.

πŸ“Š Reporter

The reporter aggregates leaks across all test environments and prints:

The environment writes temporary reports to disk and the reporter reads them.

The reporter can be configured using standard Jest reporter configuration syntax.

Options:

export default {
  reporters: [
    'default',
    [
      'jest-doctor/reporter',
      {
        tmpDir: 'custom-dir',
        jsonFile: 'report.json',
      },
    ],
  ],
};

⚠️ Limitations and known edge cases

No it.concurrent

Concurrent tests cannot be isolated reliably. jest-doctor replaces them with a synchronous version to guarantee deterministic cleanup.

No done callbacks or generators

Callback-style async and generators are legacy patterns and are not supported to keep the implementation reliable and maintainable.

Environment-dependent results

Promise scheduling differs by OS and Node version, so exact leak ordering and grouping may vary.

Microtasks resolving in same tick are not tracked

This is a JavaScript limitation, not specific to jest-doctor.

Promise.resolve().then(() => {
  /* i am not tracked as unresolved */
});

Concurrent promise combinators with nested async are problematic

Promise.race, Promise.any, Promise.all cannot safely untrack nested async:

const doSomething = async () => {
  // both promises will be tracked and never released
  await someAsyncTask();
  return new Promise(() => {
    setTimeout(resolve, 10);
  });
};

const p1 = Promise.resolve().then(() => {
  /* no problem if not async */
});

const p2 = Promise.resolve().then(
  () =>
    new Promise((resolve) => {
      /* this promise will be also always tracked */
      resolve();
    }),
);

await Promise.race([p1, p2, doSomething()]);

Imported timers bypass tracking

These timers are not intercepted. This can also be used as an escape hatch.

import { setTimeout, setInterval } from 'node:timers';

🚫 When not to use jest-doctor

In such cases, consider selectively disabling checks or using ignore rules.

πŸ’‘ Recommendations

πŸ§ͺ Tested Against

This project is tested against the following combinations:

❓ FAQ

How to migrate an existing project?

Please read the migration guide.

Why is jest-doctor so strict?

Because flaky tests cost more than broken builds.

Does this slow tests down?

Slightly. Overhead is intentional and bounded.

What is an async leak?

An async leak happens when a test starts asynchronous work but does not properly wait for or clean it up. This can:

Why does console output fail tests?

Treating console output as a leak is a deliberate strictness choice. This enforces explicit assertions and prevents silent failures in CI.

The react example shows a common problem that can be caught by tests that mock console correctly.


If jest-doctor helped you eliminate flaky tests, consider ⭐ starring the repo β€” it helps others discover the project and motivates continued development.