-
Notifications
You must be signed in to change notification settings - Fork 51k
Recover from errors with a boundary in completion phase #14104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
0aaf5e8
9b82c9f
f644cb2
0fb8440
ac7c3ab
3b93b2e
22158a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -274,11 +274,13 @@ let interruptedBy: Fiber | null = null; | |
|
|
||
| let stashedWorkInProgressProperties; | ||
| let replayUnitOfWork; | ||
| let mayReplayFailedUnitOfWork; | ||
| let isReplayingFailedUnitOfWork; | ||
| let originalReplayError; | ||
| let rethrowOriginalError; | ||
| if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { | ||
| stashedWorkInProgressProperties = null; | ||
| mayReplayFailedUnitOfWork = true; | ||
| isReplayingFailedUnitOfWork = false; | ||
| originalReplayError = null; | ||
| replayUnitOfWork = ( | ||
|
|
@@ -947,18 +949,22 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null { | |
| const siblingFiber = workInProgress.sibling; | ||
|
|
||
| if ((workInProgress.effectTag & Incomplete) === NoEffect) { | ||
| if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { | ||
| // Don't replay if it fails during completion phase. | ||
| mayReplayFailedUnitOfWork = false; | ||
| } | ||
| // This fiber completed. | ||
| // Remember we're completing this unit so we can find a boundary if it fails. | ||
| nextUnitOfWork = workInProgress; | ||
| if (enableProfilerTimer) { | ||
| if (workInProgress.mode & ProfileMode) { | ||
| startProfilerTimer(workInProgress); | ||
| } | ||
|
|
||
| nextUnitOfWork = completeWork( | ||
| current, | ||
| workInProgress, | ||
| nextRenderExpirationTime, | ||
| ); | ||
|
|
||
| if (workInProgress.mode & ProfileMode) { | ||
| // Update render duration assuming we didn't error. | ||
| stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); | ||
|
|
@@ -970,6 +976,10 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null { | |
| nextRenderExpirationTime, | ||
| ); | ||
| } | ||
| if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { | ||
| // We're out of completion phase so replaying is fine now. | ||
| mayReplayFailedUnitOfWork = true; | ||
| } | ||
| stopWorkTimer(workInProgress); | ||
| resetChildExpirationTime(workInProgress, nextRenderExpirationTime); | ||
| if (__DEV__) { | ||
|
|
@@ -1277,6 +1287,11 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void { | |
| resetContextDependences(); | ||
| resetHooks(); | ||
|
|
||
| // Reset in case completion throws. | ||
| // This is only used in DEV and when replaying is on. | ||
| const mayReplay = mayReplayFailedUnitOfWork; | ||
| mayReplayFailedUnitOfWork = true; | ||
|
|
||
| if (nextUnitOfWork === null) { | ||
| // This is a fatal error. | ||
| didFatal = true; | ||
|
|
@@ -1288,9 +1303,11 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void { | |
| (resetCurrentlyProcessingQueue: any)(); | ||
| } | ||
|
|
||
| const failedUnitOfWork: Fiber = nextUnitOfWork; | ||
| if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { | ||
| replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy); | ||
| if (mayReplay) { | ||
| const failedUnitOfWork: Fiber = nextUnitOfWork; | ||
| replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this mean that the bug only happens when in DEV? Also, couldn't this code be combined into the following block to avoid implying that the conditions/variables have any effect in production? if (__DEV__) {
// Reset global debug state
// We assume this is defined in DEV
(resetCurrentlyProcessingQueue: any)();
if (!wasCompleting && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
const failedUnitOfWork: Fiber = nextUnitOfWork;
replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
}
}
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. I pushed another commit that clarifies which parts of the fix are DEV-only. |
||
| } | ||
|
|
||
| // TODO: we already know this isn't true in some cases. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love this boolean, especially because it's not dev only. But I'm more concerned that this replay logic is getting really convoluted and it seems there must be a better way to structure it.
Maybe we can move where
replayUnitOfWorkis called instead. IfreplayUnitOfWorkis only called after a failed begin phase, we could forkbeginWorktobeginWorkInDEVwhich has a local try/catch.There might be other implications that I'm forgetting, but I think we should give this a shot before piling more complexity onto this code path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is though. I think it would get DCE’d because we never read it outside DEV. The only reason I write to it outside a flag is just because it seems awkward to do extra wrapping because of scoping.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checked — this is what I originally did, but @sebmarkbage asked that I move the extra
tryout of the hot path: #12201 (comment)I feel we should reconsider given the additional complexity of distinguishing between the begin and complete phases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed another commit that makes it clearer that part is DEV-only too. I'll look at your suggestion tomorrow/