Skip to content

Fix stale executionContext after breakpoint/alert interruption (#17355)#585

Open
everettbu wants to merge 3 commits into
mainfrom
fix/stale-execution-context-17355
Open

Fix stale executionContext after breakpoint/alert interruption (#17355)#585
everettbu wants to merge 3 commits into
mainfrom
fix/stale-execution-context-17355

Conversation

@everettbu

Copy link
Copy Markdown

Mirror of facebook/react#35865
Original author: gpardhivvarma


Summary

Fixes #17355.

When execution is interrupted by a breakpoint, alert(), or a browser debugging pause (particularly in Firefox), the finally blocks that normally reset executionContext may not run. This leaves RenderContext or CommitContext flags set even though workInProgress and workInProgressRoot are both null.

On resume, performWorkOnRoot and completeRoot see the stale flags and throw "Should not already be working."

This adds fixStaleExecutionContext() which detects this mismatch — render/commit flags are set but nothing is actually in progress — and clears just those flags before the assertion. It's called at the top of performWorkOnRoot and after flushPendingEffects in completeRoot, the two places that guard with this check.

The fix is defensive: it only activates when the context is genuinely stale, so normal execution paths are unaffected.

How did you test this change?

  • yarn prettier — no formatting issues
  • yarn linc — lint passes on changed files
  • yarn flow dom-browser — no type errors
  • yarn test packages/react-reconciler/src/__tests__/ — all 76 suites pass (1132 tests)

This edge case is inherently difficult to unit-test because it requires simulating an external interruption (breakpoint/alert) mid-render. The fix only runs when executionContext is clearly inconsistent with the actual work-in-progress state, and the existing test suite confirms it doesn't change behavior under normal conditions.

When execution is interrupted by a breakpoint, alert(), or browser
debugging pause (particularly in Firefox), the finally blocks that
normally reset executionContext may not run. This leaves RenderContext
or CommitContext flags set even though workInProgress and
workInProgressRoot are both null.

On resume, performWorkOnRoot and completeRoot see the stale flags and
throw "Should not already be working."

Add fixStaleExecutionContext() which detects this mismatch and clears
the stale render/commit flags before the assertion. Called at the top
of performWorkOnRoot and after flushPendingEffects in completeRoot.
@greptile-apps

greptile-apps Bot commented Feb 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a defensive fixStaleExecutionContext() function to detect and clear stale RenderContext/CommitContext flags on executionContext when workInProgress and workInProgressRoot are both null — a state that can occur after a debugger breakpoint, alert(), or browser pause interrupts execution before cleanup runs. The fix is placed at the top of performWorkOnRoot and after the flushPendingEffects loop in completeRoot.

  • The fix correctly identifies the root cause: execution context flags can remain set after an external interruption because renderRootSync and renderRootConcurrent restore executionContext via direct assignment (not in finally blocks).
  • However, flushPassiveEffectsImpl (line 4723) has a similar assertion ("Cannot flush passive effects while already rendering.") that is not guarded by the fix. Since flushPendingEffects is called before performWorkOnRoot in both performConcurrentWorkOnRoot and performSyncWorkOnRoot (in ReactFiberRootScheduler.js), the stale context would crash at line 4723 before the fix at line 1138 gets a chance to run.

Confidence Score: 2/5

  • The fix is incomplete — it guards two of three related assertions but misses the one most likely to fire first in the actual call chain.
  • The concept behind the fix is sound, but it doesn't fully cover the stale context scenario. The flushPassiveEffectsImpl assertion (line 4723) is reached before fixStaleExecutionContext() in both the sync and concurrent work entry points, meaning the original crash can still occur under the exact conditions this PR aims to fix.
  • packages/react-reconciler/src/ReactFiberWorkLoop.js — the flushPassiveEffectsImpl function (line 4723) and ReactFiberRootScheduler.js call sites need attention.

Important Files Changed

Filename Overview
packages/react-reconciler/src/ReactFiberWorkLoop.js Adds fixStaleExecutionContext() to clear stale RenderContext/CommitContext flags before two assertions, but misses a third assertion in flushPassiveEffectsImpl (line 4723) that is reached earlier in the call chain.

Last reviewed commit: 704dff1

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +1123 to +1131
function fixStaleExecutionContext(): void {
if (
(executionContext & (RenderContext | CommitContext)) !== NoContext &&
workInProgress === null &&
workInProgressRoot === null
) {
executionContext &= ~(RenderContext | CommitContext);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Silent state reset could mask real bugs

This function silently clears stale RenderContext/CommitContext flags without any indication that it activated. If there were ever a real bug in React's state management that left executionContext inconsistent (not caused by a debugger interruption), this fix would hide it.

Consider adding a __DEV__-mode warning when the stale context is detected and cleared, so that internal developers can distinguish between a genuine debugger interruption and an actual bug:

Suggested change
function fixStaleExecutionContext(): void {
if (
(executionContext & (RenderContext | CommitContext)) !== NoContext &&
workInProgress === null &&
workInProgressRoot === null
) {
executionContext &= ~(RenderContext | CommitContext);
}
}
function fixStaleExecutionContext(): void {
if (
(executionContext & (RenderContext | CommitContext)) !== NoContext &&
workInProgress === null &&
workInProgressRoot === null
) {
if (__DEV__) {
console.warn(
'Detected stale executionContext (%s) with no work in progress. ' +
'This is likely caused by a debugger breakpoint or alert() interrupting ' +
'a React render. Clearing stale flags.',
executionContext,
);
}
executionContext &= ~(RenderContext | CommitContext);
}
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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

Comment:
**Silent state reset could mask real bugs**

This function silently clears stale `RenderContext`/`CommitContext` flags without any indication that it activated. If there were ever a real bug in React's state management that left `executionContext` inconsistent (not caused by a debugger interruption), this fix would hide it.

Consider adding a `__DEV__`-mode warning when the stale context is detected and cleared, so that internal developers can distinguish between a genuine debugger interruption and an actual bug:

```suggestion
function fixStaleExecutionContext(): void {
  if (
    (executionContext & (RenderContext | CommitContext)) !== NoContext &&
    workInProgress === null &&
    workInProgressRoot === null
  ) {
    if (__DEV__) {
      console.warn(
        'Detected stale executionContext (%s) with no work in progress. ' +
          'This is likely caused by a debugger breakpoint or alert() interrupting ' +
          'a React render. Clearing stale flags.',
        executionContext,
      );
    }
    executionContext &= ~(RenderContext | CommitContext);
  }
}
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

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

When execution is interrupted by a breakpoint, alert(), or browser
debugging pause (particularly in Firefox), the finally blocks that
normally reset executionContext may not run. This leaves RenderContext
or CommitContext flags set even though workInProgress and
workInProgressRoot are both null.

On resume, performWorkOnRoot and completeRoot see the stale flags and
throw "Should not already be working."

Add fixStaleExecutionContext() which detects this mismatch and clears
the stale render/commit flags before the assertion. Called at the top
of performWorkOnRoot and after flushPendingEffects in completeRoot.
@everettbu
everettbu force-pushed the fix/stale-execution-context-17355 branch from c116fb9 to 214288d Compare February 25, 2026 17:33

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +1117 to +1118
// When execution is interrupted by a breakpoint, alert(), or a browser
// debugging pause, the finally blocks that reset executionContext may not run.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Misleading comment about finally blocks

The comment says "the finally blocks that reset executionContext may not run," but renderRootSync (line 2736) and renderRootConcurrent (line 3019) restore executionContext via direct assignment outside any finally block. The RenderContext flag restoration is done linearly after the work loop, not in a finally. This means the stale context scenario for RenderContext isn't about finally blocks failing — it's about execution being interrupted before the cleanup line is reached.

Only the CommitContext restorations (e.g., lines 3870, 4034) actually use try/finally. Consider updating the comment to accurately describe the mechanism:

Suggested change
// When execution is interrupted by a breakpoint, alert(), or a browser
// debugging pause, the finally blocks that reset executionContext may not run.
// When execution is interrupted by a breakpoint, alert(), or a browser
// debugging pause, the code that restores executionContext after rendering
// (which is not in a finally block) may not have run yet. When React
// resumes, executionContext still has RenderContext or CommitContext
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-reconciler/src/ReactFiberWorkLoop.js
Line: 1117-1118

Comment:
**Misleading comment about `finally` blocks**

The comment says "the finally blocks that reset executionContext may not run," but `renderRootSync` (line 2736) and `renderRootConcurrent` (line 3019) restore `executionContext` via direct assignment **outside** any `finally` block. The `RenderContext` flag restoration is done linearly after the work loop, not in a `finally`. This means the stale context scenario for `RenderContext` isn't about `finally` blocks failing — it's about execution being interrupted before the cleanup line is reached.

Only the `CommitContext` restorations (e.g., lines 3870, 4034) actually use `try`/`finally`. Consider updating the comment to accurately describe the mechanism:

```suggestion
// When execution is interrupted by a breakpoint, alert(), or a browser
// debugging pause, the code that restores executionContext after rendering
// (which is not in a finally block) may not have run yet. When React
// resumes, executionContext still has RenderContext or CommitContext
```

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

lanes: Lanes,
forceSync: boolean,
): void {
fixStaleExecutionContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fix doesn't cover the flushPassiveEffectsImpl assertion

The stale context scenario can still crash before this fix runs. Both performConcurrentWorkOnRoot (ReactFiberRootScheduler.js:546) and performSyncWorkOnRoot (ReactFiberRootScheduler.js:611) call flushPendingEffects() before calling performWorkOnRoot(). That call chain reaches flushPassiveEffectsImpl() (line 4723), which has its own assertion:

if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
  throw new Error('Cannot flush passive effects while already rendering.');
}

If executionContext is stale and there are pending passive effects, this assertion fires before fixStaleExecutionContext() at line 1138 ever executes. Similarly, the fix at line 3532 in completeRoot is placed after the do { flushPendingEffects() } while (...) loop, so the same flushPassiveEffectsImpl assertion would fire first there too.

Consider also guarding the assertion in flushPassiveEffectsImpl (line 4723), or moving the fix to a shared entry point earlier in the call chain (e.g., inside flushPendingEffects itself).

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

Comment:
**Fix doesn't cover the `flushPassiveEffectsImpl` assertion**

The stale context scenario can still crash before this fix runs. Both `performConcurrentWorkOnRoot` ([ReactFiberRootScheduler.js:546](packages/react-reconciler/src/ReactFiberRootScheduler.js)) and `performSyncWorkOnRoot` ([ReactFiberRootScheduler.js:611](packages/react-reconciler/src/ReactFiberRootScheduler.js)) call `flushPendingEffects()` **before** calling `performWorkOnRoot()`. That call chain reaches `flushPassiveEffectsImpl()` (line 4723), which has its own assertion:

```javascript
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
  throw new Error('Cannot flush passive effects while already rendering.');
}
```

If `executionContext` is stale and there are pending passive effects, this assertion fires before `fixStaleExecutionContext()` at line 1138 ever executes. Similarly, the fix at line 3532 in `completeRoot` is placed **after** the `do { flushPendingEffects() } while (...)` loop, so the same `flushPassiveEffectsImpl` assertion would fire first there too.

Consider also guarding the assertion in `flushPassiveEffectsImpl` (line 4723), or moving the fix to a shared entry point earlier in the call chain (e.g., inside `flushPendingEffects` itself).

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

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