facebook/react · error · Error

Already flushing work.

Error message

Already flushing work.

What it means

SchedulerMock's unstable_flushNumberOfYields(count) drives the queued scheduler callback in a loop until it has produced `count` yielded values (it powers assertion helpers like toFlushAndYieldThrough). Like every flush helper it sets the module-level isFlushing flag for the duration, and it throws 'Already flushing work.' on entry if another flush helper is still running, because the mock's single shared state (expectedNumberOfYields, didStop, yieldedValues) cannot back two interleaved flushes.

Source

Thrown at packages/scheduler/src/forks/SchedulerMock.js:506

function reset() {
  if (isFlushing) {
    throw new Error('Cannot reset while already flushing work.');
  }
  currentMockTime = 0;
  scheduledCallback = null;
  scheduledTimeout = null;
  timeoutTime = -1;
  yieldedValues = null;
  expectedNumberOfYields = -1;
  didStop = false;
  isFlushing = false;
  needsPaint = false;
}

// Should only be used via an assertion helper that inspects the yielded values.
function unstable_flushNumberOfYields(count: number): void {
  if (isFlushing) {
    throw new Error('Already flushing work.');
  }
  if (scheduledCallback !== null) {
    const cb = scheduledCallback;
    expectedNumberOfYields = count;
    isFlushing = true;
    try {
      let hasMoreWork = true;
      do {
        hasMoreWork = cb(true, currentMockTime);
      } while (hasMoreWork && !didStop);
      if (!hasMoreWork) {
        scheduledCallback = null;
      }
    } finally {
      expectedNumberOfYields = -1;
      didStop = false;
      isFlushing = false;
    }

View on GitHub (pinned to eafeac097b)

Solutions

  1. Make flush helper calls strictly sequential — each toFlush*/unstable_flush* call must fully return before the next one starts.
  2. Move the nested flush out of the scheduled callback or effect: run the outer flush to completion, then assert on remaining work.
  3. Remove defensive 'flush just in case' calls from shared test-utils that may execute inside an active flush.
  4. If React work itself triggers the nested flush, split the test into two top-level flushes instead of nesting.

Example fix

// before
Scheduler.scheduleCallback(NormalPriority, () => {
  Scheduler.log('a');
  Scheduler.unstable_flushNumberOfYields(1); // throws: already flushing
});
Scheduler.unstable_flushAll();

// after
Scheduler.scheduleCallback(NormalPriority, () => {
  Scheduler.log('a');
});
expect(Scheduler).toFlushAndYieldThrough(['a']); // one flush helper at a time
Defensive patterns

Strategy: validation

Validate before calling

// Guard every flush entry point with your own flag.
let flushing = false;
function safeFlushNumberOfYields(Scheduler: any, count: number) {
  if (flushing) return; // skip instead of throwing
  flushing = true;
  try {
    Scheduler.unstable_flushNumberOfYields(count);
  } finally {
    flushing = false;
  }
}

Try / catch

try {
  Scheduler.unstable_flushNumberOfYields(2);
} catch (e) {
  if (e instanceof Error && e.message.includes('Already flushing work')) {
    // a flush is already on the stack; move this call after it returns
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Invoking Scheduler.unstable_flushNumberOfYields(n) — directly or via expect(Scheduler).toFlushAndYieldThrough([...]) — from inside a callback, effect, or assertion helper that is itself being flushed by unstable_flushAll(), unstable_flushAllWithoutAsserting(), or another flush helper.

Common situations: Custom test helpers that 'ensure flushed' state by flushing at the start; component effects that flush scheduler work during act(); copy-pasted test-utils calling toFlush* helpers defensively inside teardown; nested test-runner hooks.

Related errors


AI-assisted analysis of facebook/react@eafeac097b (2026-08-21). Data as JSON: /api/errors/fc6bd707cebb081e. Report an issue: GitHub.