{"id":"00dab8fd476d8a72","repo":"jestjs/jest","slug":"ran-this-maxloops-immediates-and-there-are-st","errorCode":null,"errorMessage":"Ran ${this._maxLoops} immediates, and there are still more! Assuming we've hit an infinite recursion and bailing out...","messagePattern":"Ran (.+?) immediates, and there are still more! Assuming we've hit an infinite recursion and bailing out\\.\\.\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/jest-fake-timers/src/legacyFakeTimers.ts","lineNumber":190,"sourceCode":"          \"Assuming we've hit an infinite recursion and bailing out...\",\n      );\n    }\n  }\n\n  runAllImmediates(): void {\n    this._checkFakeTimers();\n    // Only run a generous number of immediates and then bail.\n    let i;\n    for (i = 0; i < this._maxLoops; i++) {\n      const immediate = this._immediates.shift();\n      if (immediate === undefined) {\n        break;\n      }\n      this._runImmediate(immediate);\n    }\n\n    if (i === this._maxLoops) {\n      throw new Error(\n        `Ran ${this._maxLoops} immediates, and there are still more! Assuming ` +\n          \"we've hit an infinite recursion and bailing out...\",\n      );\n    }\n  }\n\n  private _runImmediate(immediate: Tick) {\n    try {\n      immediate.callback();\n    } finally {\n      this._fakeClearImmediate(immediate.uuid);\n    }\n  }\n\n  runAllTimers(): void {\n    this._checkFakeTimers();\n    this.runAllTicks();\n    this.runAllImmediates();","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/jestjs/jest/blob/f49721c78e195558b40913977c9230f5b7f559d8/packages/jest-fake-timers/src/legacyFakeTimers.ts#L172-L208","documentation":"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.","triggerScenarios":"Calling `jest.runAllImmediates()` (legacy timers) while a `setImmediate` callback calls `setImmediate` again, e.g. a poll loop or a queue drain that never empties.","commonSituations":"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.","solutions":["Break the re-queue loop with an explicit termination condition in the callback.","Switch to modern fake timers, which provide `advanceTimersByTime` and do not require draining immediates separately.","Use bounded draining (`runOnlyPendingTimers`) instead of `runAllImmediates`.","Refactor the production code to avoid unbounded setImmediate polling."],"exampleFix":"// before\nsetImmediate(function loop() {\n  doWork();\n  setImmediate(loop);\n});\njest.runAllImmediates(); // infinite\n\n// after\nlet keepGoing = true;\nsetImmediate(function loop() {\n  doWork();\n  if (keepGoing) setImmediate(loop);\n});\nkeepGoing = false; // break before advancing\njest.runOnlyPendingTimers();","handlingStrategy":"try-catch","validationCode":"// bound setImmediate recursion\nlet n = 0;\nsetImmediate(function loop() { if (++n < 50) setImmediate(loop); });","typeGuard":null,"tryCatchPattern":"try {\n  jest.runAllImmediates();\n} catch (e) {\n  if (!/infinite recursion/.test(String(e))) throw e;\n  jest.runOnlyPendingTimers();\n}","preventionTips":["Terminate setImmediate poll loops with an explicit condition.","Use modern timers where possible.","Snapshot-drain with runOnlyPendingTimers instead of unbounded runAll."],"tags":["jest-fake-timers","legacy","infinite-loop","setimmediate"],"analyzedSha":"f49721c78e195558b40913977c9230f5b7f559d8","analyzedAt":"2026-08-03T20:16:28.571Z","schemaVersion":2}