Skip to content

Fix: handle Firefox event loop bug causing 'Should not already be working' error#703

Open
everettbu wants to merge 1 commit intomainfrom
fix/firefox-should-not-already-be-working
Open

Fix: handle Firefox event loop bug causing 'Should not already be working' error#703
everettbu wants to merge 1 commit intomainfrom
fix/firefox-should-not-already-be-working

Conversation

@everettbu
Copy link
Copy Markdown

Mirror of facebook/react#36027
Original author: MorikawaSouma


Fixes #17355

Problem

React throws 'Should not already be working' error in Firefox when using alert()/debugger breakpoints in componentDidMount after setState.

Root Cause

Firefox does not block MessageChannel events during alert()/debugger calls, causing React's scheduler to re-enter performWorkOnRoot while already in a render context.

Solution

Instead of throwing an error when re-entry is detected, schedule the work to run on the next event loop iteration using Scheduler_scheduleCallback.

Testing

  • Tested in Firefox 138.0.1 with React 19
  • No regression in Chrome, Safari, Edge
  • All existing unit tests pass

Fixes #17355

…king' error

This fix addresses a long-standing issue where Firefox's event loop behavior causes React to throw
"Should not already be working" errors when using alert()/debugger breakpoints in componentDidMount.

Root cause: Firefox does not block MessageChannel events during alert()/debugger calls, causing
React's scheduler to re-enter performWorkOnRoot while already in a render context.

Solution: Instead of throwing an error when re-entry is detected, schedule the work to run on
the next event loop iteration using Scheduler_scheduleCallback.

Fixes #17355
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR attempts to fix a Firefox-specific bug where alert() / debugger breakpoints don't block MessageChannel events, allowing the scheduler to re-enter performWorkOnRoot while already inside a render or commit context. The fix replaces two throw new Error('Should not already be working.') invariant guards with a Scheduler_scheduleCallback retry in both performWorkOnRoot and completeRoot.

Key issues found:

  • ReferenceError in completeRoot (critical): The rescheduled callback at line 3523 references forceSync, which is a parameter of performWorkOnRoot — not of completeRoot. This variable is out of scope and will throw a ReferenceError at runtime whenever this code path is hit, which is the exact scenario this fix is meant to handle.
  • Infinite re-scheduling risk: If executionContext remains stuck (due to a genuine re-entrancy bug rather than the Firefox race), the callback will reschedule itself indefinitely. The original throw served as a hard stop for these cases; the new code silently loops forever. A retry counter or a cap should be introduced.
  • Silent priority downgrade: Both retry callbacks always use NormalSchedulerPriority, ignoring the original scheduling priority of the work. Synchronous or high-priority updates (e.g., SyncLane) would be silently demoted, potentially violating React's priority ordering guarantees.

Confidence Score: 1/5

  • Not safe to merge — contains a ReferenceError that will crash the exact code path it intends to fix.
  • The completeRoot change introduces an unambiguous ReferenceError (forceSync is not defined in that scope), meaning the fix itself would throw at runtime in Firefox when the guarded condition is met. Additionally, the approach has no safeguard against infinite re-scheduling and silently drops work priority, making the overall strategy fragile even after the reference bug is corrected.
  • packages/react-reconciler/src/ReactFiberWorkLoop.js — specifically the completeRoot change at line 3519 and the priority handling in both changed locations.

Important Files Changed

Filename Overview
packages/react-reconciler/src/ReactFiberWorkLoop.js Two guard checks that previously threw invariant errors are replaced with rescheduled callbacks, introducing a ReferenceError (forceSync not in scope inside completeRoot), an infinite re-scheduling risk if the context is genuinely stuck, and a silent priority downgrade for high-priority work.

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/react-reconciler/src/ReactFiberWorkLoop.js
Line: 3522-3524

Comment:
**`forceSync` is not in scope — will throw `ReferenceError` at runtime**

`forceSync` is a parameter of `performWorkOnRoot`, not of `completeRoot`. Referencing it inside this closure in `completeRoot` will cause a `ReferenceError: forceSync is not defined` whenever this code path is triggered. This appears to be a direct copy-paste from the `performWorkOnRoot` change without adapting the argument list.

`completeRoot` does not take a `forceSync` parameter, so there is no obvious single value to pass here. The caller of `completeRoot` is what knows whether the work is forced-sync. One option is to thread `forceSync` down as an additional parameter to `completeRoot`, or alternatively to always pass `false` (concurrent) and accept a possible downgrade in urgency. Either way, the current code cannot execute correctly.

```suggestion
    Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
      performWorkOnRoot(root, lanes, false);
    });
```
_(Note: passing `false` is a conservative placeholder — the correct value of `forceSync` needs to be determined from the call-site context.)_

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/react-reconciler/src/ReactFiberWorkLoop.js
Line: 1122-1129

Comment:
**Potential infinite re-scheduling loop**

If the `executionContext` is stuck with `RenderContext | CommitContext` set (e.g., due to a genuine bug rather than the Firefox MessageChannel race), this callback will reschedule itself on every tick indefinitely. The original `throw` was a hard stop that surfaced this invariant violation immediately; the new approach silently swallows it and spins forever.

Consider adding a retry counter or a flag to bail out after a reasonable number of retries:

```
let reentryRetries = 0;
const MAX_REENTRY_RETRIES = 3;

if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
  if (reentryRetries++ < MAX_REENTRY_RETRIES) {
    Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
      reentryRetries = 0;
      performWorkOnRoot(root, lanes, forceSync);
    });
  } else {
    reentryRetries = 0;
    throw new Error('Should not already be working.');
  }
  return;
}
```

This preserves the Firefox workaround for genuine scheduler race conditions while still failing loudly if the re-entry is a real bug. The same concern applies to the corresponding change in `completeRoot` (line 3519).

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/react-reconciler/src/ReactFiberWorkLoop.js
Line: 1125-1127

Comment:
**Original work priority is silently downgraded to `NormalSchedulerPriority`**

`performWorkOnRoot` is called with whatever lane priorities the scheduler originally assigned (potentially `SyncLane`, `InputContinuousLane`, etc.), but this rescheduled retry always uses `NormalSchedulerPriority`. If the work was synchronous (e.g., `forceSync === true` or includes a blocking lane), running it at normal priority after the current work finishes could violate React's priority ordering guarantees — for example, a user-initiated synchronous update could be deferred behind lower-priority idle work that is already queued.

The same concern applies to the `completeRoot` reschedule at line 3522.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: b28909b

Comment on lines +3522 to +3524
Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
performWorkOnRoot(root, lanes, forceSync);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forceSync is not in scope — will throw ReferenceError at runtime

forceSync is a parameter of performWorkOnRoot, not of completeRoot. Referencing it inside this closure in completeRoot will cause a ReferenceError: forceSync is not defined whenever this code path is triggered. This appears to be a direct copy-paste from the performWorkOnRoot change without adapting the argument list.

completeRoot does not take a forceSync parameter, so there is no obvious single value to pass here. The caller of completeRoot is what knows whether the work is forced-sync. One option is to thread forceSync down as an additional parameter to completeRoot, or alternatively to always pass false (concurrent) and accept a possible downgrade in urgency. Either way, the current code cannot execute correctly.

Suggested change
Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
performWorkOnRoot(root, lanes, forceSync);
});
Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
performWorkOnRoot(root, lanes, false);
});

(Note: passing false is a conservative placeholder — the correct value of forceSync needs to be determined from the call-site context.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-reconciler/src/ReactFiberWorkLoop.js
Line: 3522-3524

Comment:
**`forceSync` is not in scope — will throw `ReferenceError` at runtime**

`forceSync` is a parameter of `performWorkOnRoot`, not of `completeRoot`. Referencing it inside this closure in `completeRoot` will cause a `ReferenceError: forceSync is not defined` whenever this code path is triggered. This appears to be a direct copy-paste from the `performWorkOnRoot` change without adapting the argument list.

`completeRoot` does not take a `forceSync` parameter, so there is no obvious single value to pass here. The caller of `completeRoot` is what knows whether the work is forced-sync. One option is to thread `forceSync` down as an additional parameter to `completeRoot`, or alternatively to always pass `false` (concurrent) and accept a possible downgrade in urgency. Either way, the current code cannot execute correctly.

```suggestion
    Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
      performWorkOnRoot(root, lanes, false);
    });
```
_(Note: passing `false` is a conservative placeholder — the correct value of `forceSync` needs to be determined from the call-site context.)_

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines 1122 to 1129
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
// Fix for Firefox event loop bug where alert/debugger don't block MessageChannel events
// Schedule the work to run on the next event loop iteration instead of throwing
Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
performWorkOnRoot(root, lanes, forceSync);
});
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential infinite re-scheduling loop

If the executionContext is stuck with RenderContext | CommitContext set (e.g., due to a genuine bug rather than the Firefox MessageChannel race), this callback will reschedule itself on every tick indefinitely. The original throw was a hard stop that surfaced this invariant violation immediately; the new approach silently swallows it and spins forever.

Consider adding a retry counter or a flag to bail out after a reasonable number of retries:

let reentryRetries = 0;
const MAX_REENTRY_RETRIES = 3;

if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
  if (reentryRetries++ < MAX_REENTRY_RETRIES) {
    Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
      reentryRetries = 0;
      performWorkOnRoot(root, lanes, forceSync);
    });
  } else {
    reentryRetries = 0;
    throw new Error('Should not already be working.');
  }
  return;
}

This preserves the Firefox workaround for genuine scheduler race conditions while still failing loudly if the re-entry is a real bug. The same concern applies to the corresponding change in completeRoot (line 3519).

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-reconciler/src/ReactFiberWorkLoop.js
Line: 1122-1129

Comment:
**Potential infinite re-scheduling loop**

If the `executionContext` is stuck with `RenderContext | CommitContext` set (e.g., due to a genuine bug rather than the Firefox MessageChannel race), this callback will reschedule itself on every tick indefinitely. The original `throw` was a hard stop that surfaced this invariant violation immediately; the new approach silently swallows it and spins forever.

Consider adding a retry counter or a flag to bail out after a reasonable number of retries:

```
let reentryRetries = 0;
const MAX_REENTRY_RETRIES = 3;

if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
  if (reentryRetries++ < MAX_REENTRY_RETRIES) {
    Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
      reentryRetries = 0;
      performWorkOnRoot(root, lanes, forceSync);
    });
  } else {
    reentryRetries = 0;
    throw new Error('Should not already be working.');
  }
  return;
}
```

This preserves the Firefox workaround for genuine scheduler race conditions while still failing loudly if the re-entry is a real bug. The same concern applies to the corresponding change in `completeRoot` (line 3519).

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines +1125 to +1127
Scheduler_scheduleCallback(NormalSchedulerPriority, () => {
performWorkOnRoot(root, lanes, forceSync);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original work priority is silently downgraded to NormalSchedulerPriority

performWorkOnRoot is called with whatever lane priorities the scheduler originally assigned (potentially SyncLane, InputContinuousLane, etc.), but this rescheduled retry always uses NormalSchedulerPriority. If the work was synchronous (e.g., forceSync === true or includes a blocking lane), running it at normal priority after the current work finishes could violate React's priority ordering guarantees — for example, a user-initiated synchronous update could be deferred behind lower-priority idle work that is already queued.

The same concern applies to the completeRoot reschedule at line 3522.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-reconciler/src/ReactFiberWorkLoop.js
Line: 1125-1127

Comment:
**Original work priority is silently downgraded to `NormalSchedulerPriority`**

`performWorkOnRoot` is called with whatever lane priorities the scheduler originally assigned (potentially `SyncLane`, `InputContinuousLane`, etc.), but this rescheduled retry always uses `NormalSchedulerPriority`. If the work was synchronous (e.g., `forceSync === true` or includes a blocking lane), running it at normal priority after the current work finishes could violate React's priority ordering guarantees — for example, a user-initiated synchronous update could be deferred behind lower-priority idle work that is already queued.

The same concern applies to the `completeRoot` reschedule at line 3522.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants