Skip to content
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

Merged
merged 19 commits into from
Dec 1, 2016
Merged

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Nov 16, 2016

Error boundaries, take 4 :) Supersedes #8227.

  • 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.

TODO:

  • Rebase on latest master
  • Fix last failing test
  • Ensure the context stack correctly unwinds on errors

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.

@acdlite
Copy link
Collaborator Author

acdlite commented Nov 17, 2016

Ready for review.

I may try to remove the need to unwind the context stack by moving the memoizedProps assignment from beginWork to completeWork. Without that change, if you reset to the root as we discussed (#8272 (comment)), the parent fibers re-render because they never received memoizedProps.

Don't think that should block this PR from being merged, though.

@acdlite acdlite changed the title [WIP][Fiber] New error boundary semantics [Fiber] New error boundary semantics Nov 17, 2016
}
if (primaryEffectTag & Err) {
primaryEffectTag ^= Err;
}
Copy link
Collaborator

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;

@@ -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', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unintentional?

Copy link
Collaborator

@gaearon gaearon left a 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.

@acdlite
Copy link
Collaborator Author

acdlite commented Nov 17, 2016

Fixed the hanging test; still working on the "cannot commit tree" one

@acdlite acdlite force-pushed the fibererroreffect branch 2 times, most recently from 1745719 to f64ff30 Compare November 18, 2016 10:17
@gaearon gaearon dismissed their stale review November 18, 2016 13:49

outdated

commitLifeCycles(current, effectfulFiber);
} finally {
priorityContext = previousPriorityContext;
}
Copy link
Collaborator

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 &&
Copy link
Collaborator

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 {
Copy link
Collaborator

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.'
);
Copy link
Collaborator

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).

Copy link
Collaborator

@gaearon gaearon left a 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.

Copy link
Collaborator

@gaearon gaearon left a 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);
Copy link
Collaborator

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).

Copy link
Collaborator Author

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?

Copy link
Collaborator Author

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

Copy link
Collaborator

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?

Copy link
Collaborator Author

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

acdlite and others added 10 commits November 30, 2016 17:14
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"
gaearon and others added 5 commits November 30, 2016 18:31
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
const current = effectfulFiber.alternate;
commitWork(current, effectfulFiber);
break;
try {
Copy link
Collaborator

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.

Copy link
Collaborator Author

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

Copy link
Collaborator

@gaearon gaearon left a 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.
Copy link
Collaborator

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ReactDOM.render(null, el);

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

@sebmarkbage sebmarkbage left a 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.

@acdlite acdlite merged commit e7eed58 into facebook:master Dec 1, 2016
@acdlite
Copy link
Collaborator Author

acdlite commented Dec 1, 2016

Travis takes forever. Merging anyway.

acusti pushed a commit to brandcast/react that referenced this pull request Mar 15, 2017
* 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
laurinenas pushed a commit to laurinenas/react that referenced this pull request May 28, 2018
* 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
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.

5 participants