jestjs/jest · error · Error

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

Error message

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

What it means

runAllTimers drains ticks, immediates, and then the timer map repeatedly for up to `_maxLoops` iterations. Each iteration runs the soonest timer; if new timers keep being scheduled (e.g. an interval or a setTimeout that reschedules itself), the loop never converges, so jest throws at legacyFakeTimers.ts:238 to avoid an infinite hang.

Source

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

      const [nextTimerHandle, expiry] = nextTimerHandleAndExpiry;
      this._now = expiry;
      this._runTimerHandle(nextTimerHandle);

      // Some of the immediate calls could be enqueued
      // during the previous handling of the timers, we should
      // run them as well.
      if (this._immediates.length > 0) {
        this.runAllImmediates();
      }

      if (this._ticks.length > 0) {
        this.runAllTicks();
      }
    }

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

  runOnlyPendingTimers(): void {
    // We need to hold the current shape of `this._timers` because existing
    // timers can add new ones to the map and hence would run more than necessary.
    // See https://github.com/jestjs/jest/pull/4608 for details
    const timerEntries = [...this._timers.entries()];
    this._checkFakeTimers();
    for (const _immediate of this._immediates) this._runImmediate(_immediate);

    for (const [timerHandle, timer] of timerEntries.sort(
      ([, left], [, right]) => left.expiry - right.expiry,
    )) {
      this._now = timer.expiry;

View on GitHub (pinned to f49721c78e)

Solutions

  1. Clear the interval/recursive timeout before running all timers, or use `runOnlyPendingTimers()` which snapshots the current set.
  2. Switch to modern fake timers and use `advanceTimersByTime(ms)` for a bounded advance.
  3. If you genuinely need an interval, assert on a fixed number of ticks: `for (let i=0;i<5;i++) jest.advanceTimersByTime(interval)`.
  4. Refactor the code to make the recursion terminating.

Example fix

// before
setInterval(() => poll(), 100);
jest.runAllTimers(); // infinite interval

// after
const id = setInterval(() => poll(), 100);
jest.runOnlyPendingTimers(); // runs the snapshot once
clearInterval(id);
Defensive patterns

Strategy: try-catch

Validate before calling

// clear intervals before draining timers
if (intervalId) clearInterval(intervalId);
jest.runAllTimers();

Try / catch

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

Prevention

When it happens

Trigger: Calling `jest.runAllTimers()` (legacy timers) when an active `setInterval` exists, or when a `setTimeout` callback schedules another `setTimeout`. Each fired timer adds a successor so the loop runs forever.

Common situations: An interval was set up in beforeEach and not cleared; mocked animation frames via setInterval; a retry-with-backoff implemented as recursive setTimeout; legacy fake timers do not auto-detect convergence.

Related errors


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