-
Notifications
You must be signed in to change notification settings - Fork 47.5k
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
[Fiber] New error boundary semantics #8304
Conversation
0840bb1
to
3a75250
Compare
3a75250
to
4a023c8
Compare
Ready for review. I may try to remove the need to unwind the context stack by moving the Don't think that should block this PR from being merged, though. |
4e8844c
to
ffdc8cd
Compare
} | ||
if (primaryEffectTag & Err) { | ||
primaryEffectTag ^= Err; | ||
} |
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.
const NotCallbackOrError = ~(Callback | Err);
let primaryEffectTag = effectfulFiber.effectTag & NotCallbackOrError;
7e8791b
to
a8703e6
Compare
a8703e6
to
f01a6e0
Compare
@@ -332,7 +332,7 @@ describe('ReactComponent', () => { | |||
expect(callback.mock.calls.length).toBe(3); | |||
}); | |||
|
|||
it('throws usefully when rendering badly-typed elements', () => { | |||
xit('throws usefully when rendering badly-typed elements', () => { |
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.
Unintentional?
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.
Need to fix the hanging test & the test that fails with "cannot commit tree" that didn't fail with this message before.
Fixed the hanging test; still working on the "cannot commit tree" one |
1745719
to
f64ff30
Compare
commitLifeCycles(current, effectfulFiber); | ||
} finally { | ||
priorityContext = previousPriorityContext; | ||
} |
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.
Removing this try/finally block seems responsible for some of the test failures.
If I add it back I get:
--- a/scripts/fiber/tests-passing.txt
+++ b/scripts/fiber/tests-passing.txt
@@ -470,7 +470,6 @@ src/renderers/dom/__tests__/ReactDOMProduction-test.js
* should use prod React
* should handle a simple flow
* should call lifecycle methods
-* should throw with an error code in production
src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
* should render strings as children
@@ -1021,8 +1020,12 @@ src/renderers/shared/shared/event/eventPlugins/__tests__/ResponderEventPlugin-te
src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js
* should throw when supplying a ref outside of render method
-* should support refs on owned components
+* should warn when children are mutated during update
* should not have refs on unmounted components
+* should support new-style refs
+* should support new-style refs with mixed-up owners
+* should call refs at the correct time
+* fires the callback after a component is rendered
src/renderers/shared/stack/reconciler/__tests__/ReactComponentLifeCycle-test.js
* should not reuse an instance when it has been unmounted
@@ -1185,8 +1188,9 @@ src/renderers/shared/stack/reconciler/__tests__/ReactStatelessComponent-test.js
* should update stateless component
* should unmount stateless component
* should pass context thru stateless component
-* should provide a null ref
+* should use correct name in key warning
* should support default props and prop types
+* should receive context
* should work with arrow functions
* should allow simple functions to return null
* should allow simple functions to return false
return ( | ||
fiber.tag === ClassComponent && | ||
// Instance might be null, if the fiber errored during construction | ||
fiber.stateNode && |
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.
Is this only true during unwinding? Can it be null in any other place in the code? We rely on stateNode
existing on classes in a lot of places so I'm just trying to understand if something else could fail now.
@@ -54,8 +54,8 @@ describe('ReactIncrementalErrorHandling', () => { | |||
|
|||
it('propagates an error from a noop error boundary', () => { | |||
class NoopBoundary extends React.Component { |
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.
This doesn't match behavior of NoopBoundary
in other test file. Can we either rename this one to RethrowBoundary
, or even rename both and change the other file's tests for rethrowing? That's what they were testing previously.
expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); | ||
expect(container.firstChild.textContent).toBe( | ||
ReactDOMFeatureFlags.useFiber ? '' : 'Caught an error: Hello.' | ||
); |
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.
If you change NoopBoundary
in this file to rethrow (and maybe rename it) then you won't need this condition. I would prefer that we have separate tests for "noop" and "rethrow" behavior now that they're different. It's fine to change most cases to test "rethrow" behavior (since that's what they were testing originally), and add one or two new tests for noop behavior (copy & paste + replace would be fine).
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.
Accepting on the condition that you fix newly failing tests and especially their flakiness (that they pass individually but fail together) and ensure rethrow behavior is still being tested in ErrorBoundaries-test.
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'm really worried about these Cannot commit the same tree as before.
errors. I'd prefer we don't merge this before they are addressed. I'm worried they'll slow us down because they make trees inconsistent between tests, and a failure in one test starts to affect other tests in weird ways. This wasn't the case in the previous version.
@@ -355,7 +385,15 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) { | |||
ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress); | |||
} | |||
// See if beginning this work spawns more work. | |||
let next = beginWork(current, workInProgress, nextPriorityLevel); | |||
let next; | |||
const isFailedWork = capturedErrors && capturedErrors.has(workInProgress); |
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.
Can we fork performUnitOfWork
into a separate function then call it synchronous in a while loop while there is still capturedErrors? That way we don't have to check this condition thousands of times per frame (and whatever else optimizations can be done as a result).
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 thought we made the decision that recovering from error work should use whatever priority created the error in the first place?
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.
Hmm I think I may see how this would work, I'll try it out
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.
So this is part of the follow up?
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.
Yeah I'm working on it now
This avoids the need to create an export for every combination of bits.
We'll consolidate all these blocks in a future PR that refactors the commit phase to be separate from the perform work loop.
These tests are currently failing: ✕ catches render error in a boundary during partial deferred mounting ✕ catches render error in a boundary during animation mounting ✕ propagates an error from a noop error boundary during full deferred mounting ✕ propagates an error from a noop error boundary during partial deferred mounting ✕ propagates an error from a noop error boundary during animation mounting The observed behavior is that unstable_handleError() unexpected gets called twice: "ErrorBoundary render success", "BrokenRender", "ErrorBoundary unstable_handleError", + "ErrorBoundary render success", + "BrokenRender", + "ErrorBoundary unstable_handleError", "ErrorBoundary render error"
It was fixed in facebook#8451.
b6d6017
to
778183e
Compare
This is important so that the test at the end of performAndHandleErrors() knows it's safe to rethrow.
An error thrown from a host container should be "captured" by the host container itself
8049226
to
7bc10c0
Compare
const current = effectfulFiber.alternate; | ||
commitWork(current, effectfulFiber); | ||
break; | ||
try { |
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.
These try blocks should be outside the while loop. That way we can hoist them out of this function and avoid going in and out of an deopt path for each iteration.
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.
Is my latest commit okay? Had to add another loop outside so that we can continue after an error
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'm good with this
if (failedWork) { | ||
// Host containers are a special case. If the failed work itself is a host | ||
// container, then it acts as its own boundary. In all other cases, we | ||
// ignore the work itself and only search through the parents. |
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.
When does this happen? If a callback to render throws?
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.
ReactDOM.render(null, el);
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.
If so, I'm not sure if we need to bother with this special case much since it happens at the end after all other work so there's no interleaving issue. Maybe for batching across roots but I'm not sure if this solution is sufficient in that case yet anyway.
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 was added to fix the multiple roots issue.
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.
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.
ok, sorry for the delay. This should be good to go and we'll follow up with the other fix later.
Travis takes forever. Merging anyway. |
* Exit early in scheduleUpdate if a node's priority matches This is a performance optimization and is unobservable. However, it helps protect against regressions on the following invariants on which it relies: - The priority of a fiber is greater than or equal to the priority of all its descendent fibers. - If a tree has pending work priority, its root is scheduled. * New error boundary semantics - Recovering from an error boundary no longer uses Task priority by default. The work is scheduled using whatever priority created the error. - Error handling is now a side-effect that happens during the commit phase. - The default behavior of an error boundary is to render null. Errors do not propagate except when an boundary fails. Conceptually, this would be like throwing an error from a catch block. - A host container is treated like a no-op error boundary. The first error captured by a host container is thrown at the end of the batch. Like with normal error boundaries, the entire tree is unmounted. * Fix broken setState callback test * Add test for "unwinding" context when an error interrupts rendering * Switch over primary effect types only This avoids the need to create an export for every combination of bits. * Only continue the work loop if the error was successfully captured * Add more tests for incremental error handling These tests are currently failing: ✕ catches render error in a boundary during partial deferred mounting ✕ catches render error in a boundary during animation mounting ✕ propagates an error from a noop error boundary during full deferred mounting ✕ propagates an error from a noop error boundary during partial deferred mounting ✕ propagates an error from a noop error boundary during animation mounting The observed behavior is that unstable_handleError() unexpected gets called twice: "ErrorBoundary render success", "BrokenRender", "ErrorBoundary unstable_handleError", + "ErrorBoundary render success", + "BrokenRender", + "ErrorBoundary unstable_handleError", "ErrorBoundary render error" * Verify batched updates get scheduled despite errors * Add try/catch/finally blocks around commit phase passes We'll consolidate all these blocks in a future PR that refactors the commit phase to be separate from the perform work loop. * NoopBoundary -> RethrowBoundary * Only throw uncaught error once there is no more work to perform * Remove outdated comment It was fixed in facebook#8451. * Record tests * Always reset nextUnitOfWork on error This is important so that the test at the end of performAndHandleErrors() knows it's safe to rethrow. * Add a passing test for unmounting behavior on crashed tree * Top-level errors An error thrown from a host container should be "captured" by the host container itself * Remove outdated comment * Separate Rethrow and Noop scenarios in boundary tests * Move try block outside the commit loops
* Exit early in scheduleUpdate if a node's priority matches This is a performance optimization and is unobservable. However, it helps protect against regressions on the following invariants on which it relies: - The priority of a fiber is greater than or equal to the priority of all its descendent fibers. - If a tree has pending work priority, its root is scheduled. * New error boundary semantics - Recovering from an error boundary no longer uses Task priority by default. The work is scheduled using whatever priority created the error. - Error handling is now a side-effect that happens during the commit phase. - The default behavior of an error boundary is to render null. Errors do not propagate except when an boundary fails. Conceptually, this would be like throwing an error from a catch block. - A host container is treated like a no-op error boundary. The first error captured by a host container is thrown at the end of the batch. Like with normal error boundaries, the entire tree is unmounted. * Fix broken setState callback test * Add test for "unwinding" context when an error interrupts rendering * Switch over primary effect types only This avoids the need to create an export for every combination of bits. * Only continue the work loop if the error was successfully captured * Add more tests for incremental error handling These tests are currently failing: ✕ catches render error in a boundary during partial deferred mounting ✕ catches render error in a boundary during animation mounting ✕ propagates an error from a noop error boundary during full deferred mounting ✕ propagates an error from a noop error boundary during partial deferred mounting ✕ propagates an error from a noop error boundary during animation mounting The observed behavior is that unstable_handleError() unexpected gets called twice: "ErrorBoundary render success", "BrokenRender", "ErrorBoundary unstable_handleError", + "ErrorBoundary render success", + "BrokenRender", + "ErrorBoundary unstable_handleError", "ErrorBoundary render error" * Verify batched updates get scheduled despite errors * Add try/catch/finally blocks around commit phase passes We'll consolidate all these blocks in a future PR that refactors the commit phase to be separate from the perform work loop. * NoopBoundary -> RethrowBoundary * Only throw uncaught error once there is no more work to perform * Remove outdated comment It was fixed in facebook#8451. * Record tests * Always reset nextUnitOfWork on error This is important so that the test at the end of performAndHandleErrors() knows it's safe to rethrow. * Add a passing test for unmounting behavior on crashed tree * Top-level errors An error thrown from a host container should be "captured" by the host container itself * Remove outdated comment * Separate Rethrow and Noop scenarios in boundary tests * Move try block outside the commit loops
Error boundaries, take 4 :) Supersedes #8227.
default. The work is scheduled using whatever priority created the
error.
commit phase.
do not propagate except when an boundary fails. Conceptually, this would
be like throwing an error from a catch block.
error captured by a host container is thrown at the end of the batch.
Like with normal error boundaries, the entire tree is unmounted.
TODO:
The scheduler is getting a bit messy / difficult to work with, especially with all the
perform-
functions. I'll try to clean some of the accumulated cruft in a future PR.