{"id":"df2921930a4254d3","repo":"jestjs/jest","slug":"ran-this-maxloops-ticks-and-there-are-still-m","errorCode":null,"errorMessage":"Ran ${this._maxLoops} ticks, and there are still more! Assuming we've hit an infinite recursion and bailing out...","messagePattern":"Ran (.+?) ticks, 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":170,"sourceCode":"    let i;\n    for (i = 0; i < this._maxLoops; i++) {\n      const tick = this._ticks.shift();\n\n      if (tick === undefined) {\n        break;\n      }\n\n      if (\n        !Object.prototype.hasOwnProperty.call(this._cancelledTicks, tick.uuid)\n      ) {\n        // Callback may throw, so update the map prior calling.\n        this._cancelledTicks[tick.uuid] = true;\n        tick.callback();\n      }\n    }\n\n    if (i === this._maxLoops) {\n      throw new Error(\n        `Ran ${this._maxLoops} ticks, and there are still more! ` +\n          \"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","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/jestjs/jest/blob/f49721c78e195558b40913977c9230f5b7f559d8/packages/jest-fake-timers/src/legacyFakeTimers.ts#L152-L188","documentation":"runAllTicks drains the `_ticks` queue (process.nextTick callbacks queued under legacy fake timers) for at most `_maxLoops` iterations (default 100,000). If the queue still is not empty after that, it assumes an infinite tick-recursion loop and throws at legacyFakeTimers.ts:170 to avoid hanging the process.","triggerScenarios":"Calling `jest.runAllTicks()` (legacy timers) while a `process.nextTick` callback re-queues another nextTick (or a callback that transitively schedules one), so each iteration adds work faster than it is drained.","commonSituations":"Mocked code uses `process.nextTick` recursively; a library (e.g. some readable-stream implementations) schedules follow-up ticks; switching from modern to legacy timers without adjusting the test; an event emitter that re-emits on nextTick.","solutions":["Find the callback that re-queues a tick and break the cycle (clear a flag, return early after first run).","Prefer modern fake timers (`jest.useFakeTimers()` without `legacyFakeTimers: true`) which handle this more gracefully.","Use `runOnlyPendingTimers`/bounded advances instead of unbounded `runAllTicks`.","Raise `maxLoops` only after confirming the recursion is intentional and finite."],"exampleFix":"// before\nlet pending = true;\nprocess.nextTick(function cb() {\n  if (pending) { pending = false; process.nextTick(cb); }\n});\njest.runAllTicks(); // infinite\n\n// after\nlet count = 0;\nprocess.nextTick(function cb() {\n  if (++count < 3) process.nextTick(cb);\n});\njest.runOnlyPendingTimers();","handlingStrategy":"try-catch","validationCode":"// cap tick recursion in the code under test\nlet n = 0;\nprocess.nextTick(function cb() { if (++n < 100) process.nextTick(cb); });","typeGuard":null,"tryCatchPattern":"try {\n  jest.runAllTicks();\n} catch (e) {\n  if (!/infinite recursion/.test(String(e))) throw e;\n  // fall back to bounded draining\n  jest.runOnlyPendingTimers();\n}","preventionTips":["Avoid recursive nextTick in production code.","Prefer modern fake timers over legacy.","Clear recursion flags before advancing."],"tags":["jest-fake-timers","legacy","infinite-loop","nexttick"],"analyzedSha":"f49721c78e195558b40913977c9230f5b7f559d8","analyzedAt":"2026-08-03T20:16:28.571Z","schemaVersion":2}