jestjs/jest · error · Error

Ran ${this._maxLoops} immediates, and there are still more!

Error message

Ran ${this._maxLoops} immediates, and there are still more! Assuming we've hit an infinite recursion and bailing out...

What it means

runAllImmediates drains the `_immediates` queue for up to `_maxLoops` iterations. If immediates are still pending after 100,000 (default) iterations, jest-fake-timers concludes a setImmediate callback is re-queueing itself and throws at legacyFakeTimers.ts:190 to break the cycle.

Source

Thrown at packages/jest-fake-timers/src/legacyFakeTimers.ts:190

          "Assuming we've hit an infinite recursion and bailing out...",
      );
    }
  }

  runAllImmediates(): void {
    this._checkFakeTimers();
    // Only run a generous number of immediates and then bail.
    let i;
    for (i = 0; i < this._maxLoops; i++) {
      const immediate = this._immediates.shift();
      if (immediate === undefined) {
        break;
      }
      this._runImmediate(immediate);
    }

    if (i === this._maxLoops) {
      throw new Error(
        `Ran ${this._maxLoops} immediates, and there are still more! Assuming ` +
          "we've hit an infinite recursion and bailing out...",
      );
    }
  }

  private _runImmediate(immediate: Tick) {
    try {
      immediate.callback();
    } finally {
      this._fakeClearImmediate(immediate.uuid);
    }
  }

  runAllTimers(): void {
    this._checkFakeTimers();
    this.runAllTicks();
    this.runAllImmediates();

View on GitHub (pinned to f49721c78e)

Solutions

  1. Break the re-queue loop with an explicit termination condition in the callback.
  2. Switch to modern fake timers, which provide `advanceTimersByTime` and do not require draining immediates separately.
  3. Use bounded draining (`runOnlyPendingTimers`) instead of `runAllImmediates`.
  4. Refactor the production code to avoid unbounded setImmediate polling.

Example fix

// before
setImmediate(function loop() {
  doWork();
  setImmediate(loop);
});
jest.runAllImmediates(); // infinite

// after
let keepGoing = true;
setImmediate(function loop() {
  doWork();
  if (keepGoing) setImmediate(loop);
});
keepGoing = false; // break before advancing
jest.runOnlyPendingTimers();
Defensive patterns

Strategy: try-catch

Validate before calling

// bound setImmediate recursion
let n = 0;
setImmediate(function loop() { if (++n < 50) setImmediate(loop); });

Try / catch

try {
  jest.runAllImmediates();
} catch (e) {
  if (!/infinite recursion/.test(String(e))) throw e;
  jest.runOnlyPendingTimers();
}

Prevention

When it happens

Trigger: Calling `jest.runAllImmediates()` (legacy timers) while a `setImmediate` callback calls `setImmediate` again, e.g. a poll loop or a queue drain that never empties.

Common situations: Mocked polling code; custom task schedulers built on setImmediate; RXJS/some stream libs that yield via setImmediate; legacy timer mode where the recursion guard of modern timers is absent.

Related errors


AI-assisted analysis of jestjs/jest@f49721c78e (2026-08-03). Data as JSON: /data/errors/00dab8fd476d8a72.json. Report an issue: GitHub.