angular/angular · error · Error

flush failed after reaching the limit of ${limit} tasks. Doe

Error message

flush failed after reaching the limit of ${limit} tasks. Does your code use a polling timeout?

What it means

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.

Source

Thrown at packages/zone.js/lib/zone-spec/fake-async-test.ts:332

    if (this._schedulerQueue.length === 0) {
      return 0;
    }
    // Find the last task currently queued in the scheduler queue and tick
    // till that time.
    const startTime = this._currentTickTime;
    const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
    this.tick(lastTask.endTime - startTime, doTick);
    return this._currentTickTime - startTime;
  }

  private flushNonPeriodic(limit: number, doTick?: (elapsed: number) => void): number {
    const startTime = this._currentTickTime;
    let lastCurrentTime = 0;
    let count = 0;
    while (this._schedulerQueue.length > 0) {
      count++;
      if (count > limit) {
        throw new Error(
          'flush failed after reaching the limit of ' +
            limit +
            ' tasks. Does your code use a polling timeout?',
        );
      }

      // flush only non-periodic timers.
      // If the only remaining tasks are periodic(or requestAnimationFrame), finish flushing.
      if (
        this._schedulerQueue.filter((task) => !task.isPeriodic && !task.isRequestAnimationFrame)
          .length === 0
      ) {
        break;
      }

      const current = this._schedulerQueue.shift()!;
      lastCurrentTime = this._currentTickTime;
      this._currentTickTime = current.endTime;

View on GitHub (pinned to 51cb07e980)

Solutions

  1. Flush with an explicit higher limit when the number of timer generations is known: flush(100)
  2. 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
  3. Refactor the code under test to be bounded (max retries, takeUntil) so flush terminates
  4. Prefer tick(exactMs) with known elapsed time over flush() when the schedule is deterministic

Example fix

// before (component polls with recursive setTimeout)
refresh() { this.http.get('/status').subscribe(() => setTimeout(() => this.refresh(), 1000)); }
// test: flush(); // never drains -> limit of 20 tasks exceeded

// after (bounded flush in test)
flush(50); // known upper bound of poll generations
discardPeriodicTasks();
// or in component: use setInterval + clear on destroy so flush() treats it as periodic
Defensive patterns

Strategy: validation

Validate before calling

// in the fakeAsync test
const FLUSH_LIMIT = 50; // known upper bound of timer generations
const elapsed = flush(FLUSH_LIMIT);
discardPeriodicTasks(); // for legit remaining intervals

Try / catch

try {
  flush();
} catch (e) {
  if ((e as Error).message.includes('polling timeout')) {
    discardPeriodicTasks(); // remaining work is periodic/polling
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of angular/angular@51cb07e980 (2026-08-22). Data as JSON: /api/errors/faadf0ac893bf407. Report an issue: GitHub.