Skip to content

[Scheduler] Guard performWorkUntilDeadline against re-entrant MessageChannel events#36436

Open
Abhishek764 wants to merge 1 commit into
facebook:mainfrom
Abhishek764:fix/scheduler-reentrant-messagechannel-firefox
Open

[Scheduler] Guard performWorkUntilDeadline against re-entrant MessageChannel events#36436
Abhishek764 wants to merge 1 commit into
facebook:mainfrom
Abhishek764:fix/scheduler-reentrant-messagechannel-firefox

Conversation

@Abhishek764
Copy link
Copy Markdown

Summary

Fixes the long-standing "Should not already be working." error (invariant #327) reported in #17355. The error occurs in Firefox and IE when alert() or a debugger breakpoint is used while React is mid-render or mid-commit (e.g. inside componentDidMount).

Root cause:

Firefox and IE, unlike Chrome, allow the browser event loop to continue running while a alert() dialog is shown or a debugger statement is paused. If the Scheduler had a MessageChannel message queued (it pre-schedules one whenever it plans to yield), Firefox delivers that message immediately when the JS call stack resumes. This re-enters performWorkUntilDeadline while the first invocation is still on the call stack — with isPerformingWork === true and with executionContext still holding RenderContext | CommitContext. The invariant check in performWorkOnRoot then throws:

Error: Should not already be working.

Fix:

Add a one-line re-entrance guard at the top of performWorkUntilDeadline using the existing isPerformingWork flag:

const performWorkUntilDeadline = () => {
  if (isPerformingWork) {
    // Re-entered due to Firefox/IE allowing the event loop to continue
    // during alert() or a debugger pause. The ongoing work loop will
    // resume and handle remaining tasks when the call stack unwinds.
    return;
  }
  // ...
};

isPerformingWork is already wrapped in a try/finally inside flushWork, so it is guaranteed to reset correctly — even if the work throws. Returning early here does not lose any work: when the original invocation completes, ensureRootIsScheduled re-queues any remaining lanes as needed.

The throw inside performWorkOnRoot is intentionally kept — it protects against genuine re-entrant bugs in React's own code. This fix prevents the false positive that arises from browser-level re-entrance at the scheduler boundary.

Test Plan

  • Added a regression test in Scheduler-test.js that simulates Firefox behavior by calling performWorkUntilDeadline directly from inside a running task (simulating a MessageChannel event delivered during a debugger pause).
  • Test verifies: (1) no exception thrown, (2) task runs exactly once, (3) scheduler state is clean after.
  • All existing scheduler tests continue to pass.
PASS packages/scheduler/src/__tests__/Scheduler-test.js
  SchedulerBrowser
    ✓ task that finishes before deadline
    ✓ task with continuation
    ✓ multiple tasks
    ✓ multiple tasks with a yield in between
    ✓ cancels tasks
    ✓ throws when a task errors then continues in a new event
    ✓ schedule new task after queue has emptied
    ✓ schedule new task after a cancellation
    ✓ re-entrant message event is a no-op (Firefox alert/debugger regression)
    ✓ yielding continues in a new task regardless of how much time is remaining

Affected versions

Reproducible in React 16.x through 18.x in Firefox and IE (reported and confirmed in issue #17355).

Fixes #17355

Firefox and IE allow alert() or debugger statements to pause the JS
call stack while continuing to deliver browser event loop tasks. When
a MessageChannel message is already queued (e.g. because the scheduler
planned to yield), Firefox delivers it immediately when execution
resumes -- re-entering performWorkUntilDeadline while isPerformingWork
is still true and while react-reconciler still has RenderContext or
CommitContext set on executionContext. This caused the invariant check
in performWorkOnRoot to throw "Should not already be working."

Fix: add an early return to performWorkUntilDeadline when
isPerformingWork is true. The ongoing work loop will complete normally
and call ensureRootIsScheduled when done -- no work is lost.

Add a regression test that simulates Firefox's re-entrant behavior by
calling performWorkUntilDeadline directly from inside a running task.

Fixes facebook#17355

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@meta-cla
Copy link
Copy Markdown

meta-cla Bot commented May 8, 2026

Hi @Abhishek764!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@Abhishek764
Copy link
Copy Markdown
Author

Abhishek764 commented May 8, 2026

I identified the CLA failure was caused by signing with the wrong email initially. I’ve now re-signed the CLA using the correct email associated with my GitHub account and pushed a commit to retrigger the checks. Please let me know if anything else is needed.

@meta-cla meta-cla Bot added the CLA Signed label May 8, 2026
@meta-cla
Copy link
Copy Markdown

meta-cla Bot commented May 8, 2026

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

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.

"Should not already be working" in Firefox after a breakpoint/alert

1 participant