{"record":{"id":"faadf0ac893bf407","repo":"angular/angular","slug":"flush-failed-after-reaching-the-limit-of-limit","errorCode":null,"errorMessage":"flush failed after reaching the limit of ${limit} tasks. Does your code use a polling timeout?","messagePattern":"flush failed after reaching the limit of (.+?) tasks\\. Does your code use a polling timeout\\?","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/zone.js/lib/zone-spec/fake-async-test.ts","lineNumber":332,"sourceCode":"    if (this._schedulerQueue.length === 0) {\n      return 0;\n    }\n    // Find the last task currently queued in the scheduler queue and tick\n    // till that time.\n    const startTime = this._currentTickTime;\n    const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];\n    this.tick(lastTask.endTime - startTime, doTick);\n    return this._currentTickTime - startTime;\n  }\n\n  private flushNonPeriodic(limit: number, doTick?: (elapsed: number) => void): number {\n    const startTime = this._currentTickTime;\n    let lastCurrentTime = 0;\n    let count = 0;\n    while (this._schedulerQueue.length > 0) {\n      count++;\n      if (count > limit) {\n        throw new Error(\n          'flush failed after reaching the limit of ' +\n            limit +\n            ' tasks. Does your code use a polling timeout?',\n        );\n      }\n\n      // flush only non-periodic timers.\n      // If the only remaining tasks are periodic(or requestAnimationFrame), finish flushing.\n      if (\n        this._schedulerQueue.filter((task) => !task.isPeriodic && !task.isRequestAnimationFrame)\n          .length === 0\n      ) {\n        break;\n      }\n\n      const current = this._schedulerQueue.shift()!;\n      lastCurrentTime = this._currentTickTime;\n      this._currentTickTime = current.endTime;","sourceCodeStart":314,"sourceCodeEnd":350,"githubUrl":"https://github.com/angular/angular/blob/51cb07e98081ab7e4e84a9e0949266a8e19cca84/packages/zone.js/lib/zone-spec/fake-async-test.ts#L314-L350","documentation":"In a fakeAsync test, flush() drains the fake scheduler queue, executing queued timer callbacks while advancing virtual time. flushNonPeriodic loops until only periodic/rAF tasks remain, but if each flushed task schedules another non-periodic timer (a polling loop such as recursive setTimeout), the queue never empties; after `limit` iterations (default 20) it aborts with this error, suggesting the polling-timeout cause. It is a runaway-loop detector, not a timer bug.","triggerScenarios":"Code under fakeAsync that polls: function poll() { ...; setTimeout(poll, 1000) } with setTimeout (each flush iteration enqueues the next); retry/backoff loops with setTimeout; watch-mode style checks inside components under test; calling flush() where tick() or a bounded flush was intended.","commonSituations":"Component tests where the component starts a status-polling interval on init; websocket-reconnect logic with recursive setTimeout exercised by flush(); services with exponential retry; tests migrating from jasmine done() to fakeAsync where unbounded awaits became timer loops.","solutions":["Flush with an explicit higher limit when the number of timer generations is known: flush(100)","Use discardPeriodicTasks() after the assertions if the remaining work is a legit interval — but convert recursive setTimeout polling to setInterval so the scheduler classifies it periodic and flush stops on it","Refactor the code under test to be bounded (max retries, takeUntil) so flush terminates","Prefer tick(exactMs) with known elapsed time over flush() when the schedule is deterministic"],"exampleFix":"// before (component polls with recursive setTimeout)\nrefresh() { this.http.get('/status').subscribe(() => setTimeout(() => this.refresh(), 1000)); }\n// test: flush(); // never drains -> limit of 20 tasks exceeded\n\n// after (bounded flush in test)\nflush(50); // known upper bound of poll generations\ndiscardPeriodicTasks();\n// or in component: use setInterval + clear on destroy so flush() treats it as periodic","handlingStrategy":"validation","validationCode":"// in the fakeAsync test\nconst FLUSH_LIMIT = 50; // known upper bound of timer generations\nconst elapsed = flush(FLUSH_LIMIT);\ndiscardPeriodicTasks(); // for legit remaining intervals","typeGuard":null,"tryCatchPattern":"try {\n  flush();\n} catch (e) {\n  if ((e as Error).message.includes('polling timeout')) {\n    discardPeriodicTasks(); // remaining work is periodic/polling\n  } else throw e;\n}","preventionTips":["Use setInterval for polling so flush() treats it as periodic and stops on it","Bound retries/polling in code under test","Prefer tick(exactMs) when the schedule is deterministic; pass an explicit limit to flush()"],"tags":["fake-async","timers","polling","infinite-loop","testing"],"backgroundTag":"fake-async-polling-timeout","analyzedSha":"51cb07e98081ab7e4e84a9e0949266a8e19cca84","analyzedAt":"2026-08-22T07:04:54.531Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}