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

Add stack unwinding phase for handling errors #12201

Merged
merged 8 commits into from
Feb 24, 2018

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Feb 9, 2018

A rewrite of error handling, with semantics that more closely match stack unwinding.

Errors that are thrown during the render phase unwind to the nearest error boundary, like before. But rather than synchronously unmount the children before retrying, we restart the failed subtree within the same render phase. The failed children are still unmounted (as if all their keys changed) but without an extra commit.

Commit phase errors are different. They work by scheduling an error on the update queue of the error boundary. When we enter the render phase, the error is popped off the queue. The rest of the algorithm is the same.

This approach is designed to work for throwing non-errors, too, though that feature is not implemented yet.

Known differences from existing error handling semantics in synchronous mode:

  • When a component throws an error, we continue rendering its siblings rather than unwind immediately.
  • Because of the previous point, a single error boundary may capture more than one error during a single render, if multiple child branches fail.
  • We call componentDidCatch for each error that a boundary captures, instead of just the first one.
  • Error boundaries only catch error objects. Non-errors will propagate to the root and cause the tree to unmount, as if it’s an uncaught error.

Unresolved items:

  • Add getDerivedStateFromCatch lifecycle behind a feature flag. This is fired during the render phase so that we can retry without committing, as opposed to componentDidCatch, which fires in the commit phase. We'll keep componentDidCatch around for logging purposes. If getDerivedStateFromCatch is not implemented but componentDidCatch is, the fallback behavior is to unmount all the boundary's children. (This part is already implemented.) We may warn about the missing getDerivedStateFromCatch in a future release.
  • Existing code that schedules recovery updates inside componentDidCatch will have mostly the same semantics, with one key exception. We no longer track whether an error boundary failed in a previous commit. So it's now possible to fall into a recursive error loop, in the same way as an unguarded setState inside componentDidUpdate.

@@ -784,7 +788,7 @@ describe('ReactErrorBoundaries', () => {
]);
});

it('propagates errors on retry on mounting', () => {
xit('propagates errors on retry on mounting', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is related to item 2 in the "Unresolved items" section. Waiting for feedback on whether this is too drastic a change and if we should hew more closely to the existing retry semantics.

@bvaughn
Copy link
Contributor

bvaughn commented Feb 9, 2018

Before reviewing, I'm curious: are these changes intended for (or necessary for) 16.3?

I ask because the change is kind of large, and I'm wondering what the impact on timing would be (e.g. time to review, sync and test internally, update more documentation, etc.)

@gaearon
Copy link
Collaborator

gaearon commented Feb 9, 2018

Existing code that schedules recovery updates inside componentDidCatch will have mostly the same semantics, with one key exception. We no longer track whether an error boundary failed in a previous commit. So it's now possible to fall into a recursive error loop, in the same way as an unguarded setState inside componentDidUpdate.

If we were to do this, should the boundary also handle its own errors? Our justification for not doing it was that this leads to loops. But now we have loops anyway?

@acdlite
Copy link
Collaborator Author

acdlite commented Feb 9, 2018

@gaearon Talked it through with @sebmarkbage. What we'll do is keep the failedErrorBoundaries tracking around for legacy purposes, until we can migrate people to the new strategy in React 17. Unless the boundary defines getDerivedStateFromCatch, which opts you into the new behavior.

if (value instanceof Error) {
algebraicEffectTag = Err;
} else if (value !== null && typeof value === 'object') {
// If instanceof fails, fall back to duck typing.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This feels surprising. Moving code into an iframe could fundamentally change the behavior if it used to pass the first check but doesn't pass the second one now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe we should only do duck typing and skip instanceof entirely?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This would make sense to me. I would maybe check just stack since it's added by the browser, but message could potentially be empty (I think?)

On the other hand is it plausible that Promise ever gets a stack?

Also, what happens if you just throw a string? It's bad but it can happen. We used to handle this, do we not anymore?

My impression is that the plan is to treat thenables specially, and treat everything else as an error. Why is the logic written around special casing the error object?

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 want to leave the door open for supporting custom algebraic effects. Error boundaries are a normal effect boundary that pattern matches on errors; there’s another boundary that pattern matches on promises, and so on. That’s what these lines here are intended to model.

Error boundaries will no longer catch null or undefined, but the root still will (that’s why the Unknown type exists).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do error boundaries still catch strings?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No they only catch errors. A string will propagate to the root. I’ll add this to the list of known deviations.

If this is too drastic a change I’m fine with for holding off on that part until React 17. Though if we do that, we should warn so people don’t rely on that behavior.

Copy link
Collaborator

Choose a reason for hiding this comment

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

To be honest I'd say it's okay to say this was unsupported. It's a known bad pattern, and error boundaries should not (yet :-) be used for control flow anyway. We always suggested they're meant to be used for exceptional situations.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is quite ok for a breaking change. The cross-realm issue and the fact that throwing strings, while not good practice, is still common.

Thenables are a lot more restrictive since they will essentially always unwrap through any async boundary, they're pretty useless for anything other than as promises. So limiting this to thenable would be a lot safer.

I understand that our general strategy is to attempt to limit componentDidCatch to errors only but not quite sure about the mechanism here.

@acdlite
Copy link
Collaborator Author

acdlite commented Feb 10, 2018

@bvaughn It would nice to get this into 16.3 because the changes we're planning for 16.4 depend on it.

@@ -42,6 +39,9 @@ export type FiberRoot = {
pendingContext: Object | null,
// Determines if we should attempt to hydrate on the initial mount
+hydrate: boolean,
// Remaining expiration time on this root.
// TODO: List this into the renderer
Copy link
Collaborator

Choose a reason for hiding this comment

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

Typo

let thrownValue = value;
// Check if the thrown value is an error. If it is, we will replay the
// begin phase using invokeGuardedCallback, so that the error is not
// treated as caught by the debugger.
Copy link
Collaborator

Choose a reason for hiding this comment

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

😮

typeof value.stack === 'string')
) {
// Retore the original state of the work-in-progress
Object.assign(workInProgress, stashedWorkInProgressProperties);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not a super big fan of this. I usually grep for assignments when debugging to figure out what piece of code changed a field. On the other hand the code is smaller..

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 don't love it either but all the alternatives I considered were worse. I can try to convince you out of band if needed :D

Copy link
Collaborator

Choose a reason for hiding this comment

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

// We assume any use of `deferredUpdates` is to split a high-priority
// interactive update into separate high and low values. Set this to true so
// that the low-pri value is treated as if it were the result of an
// interaction, even if we're in the render phase.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't understand this, can you explain more? Does this fix the issue I reported or is it unrelated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Meant to delete this, not needed

'BadMount',
'componentDidCatch: Error 1',
'componentDidCatch: Error 2',
'componentDidCatch: Error 3',
Copy link
Collaborator

Choose a reason for hiding this comment

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

This makes sense theoretically but how practical is it? Once we're in an error state it's likely other things can break due to mistaken assumptions with cryptic errors that can't be reproduced otherwise.

I guess a logger could remember whether it saw an error earlier, and attach that as metadata.

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 think in most cases, if you catch multiple errors, it will be because they can from different siblings. (Another difference between this implementation and the previous one is that if there's an error, we continue rendering the siblings before we unwind.) In that case, you'd want to log all three errors.

Copy link
Collaborator Author

@acdlite acdlite Feb 10, 2018

Choose a reason for hiding this comment

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

Realizing this test made more sense when it was using the render phase version of the lifecycle. I'll rewrite. Never mind, I still think this makes sense. Three siblings, three errors.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it makes sense to start dealing with this because it is less confusing than sometimes getting different errors depending on which sibling got furthest.


// Bits greater than Incomplete are used to identify fibers that threw. These
// are like algebraic effects. These bits are mutually exclusive.
export const Combined = /* */ 0b010000000000;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you explain why this is called "combined"? What is being combined here? Isn't the first effect that we "catch" the only one? Or do we attempt to render siblings as deep as we can and then "wait on all"?

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 think I'll just get rid of this, but the idea was that you could dispatch multiple errors at the same time. It's only being used for unhandled errors at the root. Don't think it's necessary.

@acdlite acdlite force-pushed the error-handling-refactor branch from 2570d09 to dfe3ab3 Compare February 10, 2018 00:24
@acdlite
Copy link
Collaborator Author

acdlite commented Feb 10, 2018

If you're looking for a place to start, most of the interesting stuff happens in a new module called ReactFiberIncompleteWork. This is where we handle unwinding an incomplete component stack. https://github.com/acdlite/react/blob/1f38a7b1b5acb3c2b7a3a075c5358b934ef6d50e/packages/react-reconciler/src/ReactFiberIncompleteWork.js

@acdlite acdlite force-pushed the error-handling-refactor branch from 1f38a7b to 06b7db4 Compare February 10, 2018 03:16
@gaearon
Copy link
Collaborator

gaearon commented Feb 10, 2018

So.. this is kinda huge.

screen shot 2018-02-10 at 13 55 51

screen shot 2018-02-10 at 13 56 13

I understand it's a prerequisite for blockers so we probably have to do it. But is there any way we can reduce the amount of code here? Or is there a plan for how we can do it later?

How "easy" would blockers be after this, in terms of code size?

@gaearon
Copy link
Collaborator

gaearon commented Feb 10, 2018

It's less dramatic with error extraction (compared against master without extraction).

screen shot 2018-02-10 at 14 26 08

Still pretty large.

@acdlite
Copy link
Collaborator Author

acdlite commented Feb 10, 2018

The biggest code additions are the ReactFiberIncompleteWork module, which handles stack unwinding, and the uncommenting of resumeMountClassInstance, since error boundaries are now a form of resuming. Both overlap heavily with planned features, but the flip side of that is if we weren’t planning to eventually add those, this PR could be structured in a different way to make the code smaller.

There are several places where some more abstraction (or better abstraction) could cut down on repetition and reduce code size, particularly in ReactFiberClassComponent. But I’d like to receive feedback on the overall architecture before I start optimizing too much.

break;
case IndeterminateComponent:
break;
default:
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 code path commonly exercised in tests? Is there a risk that we'll add a new tag, forget to add it to this switch, and it blows up only on the error path (which is rare enough that we will miss it)?

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’s triggered whenever anything throws, so we need test cases that throw for every possible type. Working on adding this to the fuzz tester.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like a good candidate for a Flow exhaustiveness check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True. Though we still should test every path to prevent stack push/pop mismatch bugs, like we’ve seen in the past.

@@ -139,6 +139,8 @@ export type Fiber = {|
firstEffect: Fiber | null,
lastEffect: Fiber | null,

thrownValue: any,
Copy link
Collaborator

Choose a reason for hiding this comment

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

You'll have to make the case for this addition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Strongest case I think is resuming. We should be able to reuse parts of the effect list during a bail out, including any of the values that were thrown.

Can you think of some viable alternatives? I'm having trouble coming up with one.

it('does not break with a bad Map polyfill', () => {
// We don't currently use fibers as keys. Re-enable this test if we
// ever do again.
it.skip('does not break with a bad Map polyfill', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does it matter having this enabled? We will definitely not enable this if we ever do again. :)

(Which might be this diff instead of the extra field.)

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 can reenable, but if we ever do this again, we'll have to ensure that this test triggers whatever path adds a fiber to Map/Set. So I think we'll need to think about it regardless.

'BadMount',
'componentDidCatch: Error 1',
'componentDidCatch: Error 2',
'componentDidCatch: Error 3',
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it makes sense to start dealing with this because it is less confusing than sometimes getting different errors depending on which sibling got furthest.

@@ -380,6 +380,7 @@ describe('ReactDOM', () => {
it('throws in DEV if jsdom is destroyed by the time setState() is called', () => {
class App extends React.Component {
state = {x: 1};
componentDidUpdate() {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is this for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There's a comment below. It's used to trigger invokeGuardedCallback, which no longer wraps the begin phase unless an error is thrown.

workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
workInProgress.thrownValue = null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why did this move? We should be careful not to add things to things that happen in the hottest paths unless they always need to happen. If they sometimes need to happen, it should only happen in those paths.

@@ -148,6 +163,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
workInProgress,
current.child,
nextChildren,
deleteExistingChildren,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Like discussed offline. Let's call the reconciler twice instead of using boolean flag, first with nextChildren = null, then with current.child = null.

@@ -237,19 +253,22 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
const hasContext = pushLegacyContextProvider(workInProgress);
const instance = workInProgress.stateNode;
Copy link
Collaborator

Choose a reason for hiding this comment

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

You're not using the instance. Let's inline it back in place. It avoids reading the stateNode property here when current is not null.

fiber: Fiber,
startTime: ExpirationTime,
Copy link
Collaborator

Choose a reason for hiding this comment

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

rm until we need it

break;
case Fragment:
break;
case Mode:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we just remove all these branches that do nothing?

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 guess so. I thought maybe the “unknown tag” invariant would reduce the likelihood that we’d overlook one of the types but it doesn’t provide that much safety. Need tests regardless.

@acdlite acdlite force-pushed the error-handling-refactor branch 4 times, most recently from cee3941 to 401ab7b Compare February 20, 2018 12:02
@acdlite acdlite force-pushed the error-handling-refactor branch 5 times, most recently from 2ca1f63 to d889f97 Compare February 20, 2018 12:52
@acdlite
Copy link
Collaborator Author

acdlite commented Feb 20, 2018

@sebmarkbage Ready for another look

// begin phase using invokeGuardedCallback, so that the error is not
// treated as caught by the debugger.
if (
value instanceof Error ||
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's inverse this check to look for thenables instead.

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'll remove it entirely until the next PR

expirationTime,
) {
const stashedWorkInProgressProperties = Object.assign({}, workInProgress);
try {
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 move this out of the hot loop and instead be around workLoop?

@acdlite acdlite force-pushed the error-handling-refactor branch from ddac776 to 750bae0 Compare February 24, 2018 01:19
@acdlite acdlite force-pushed the error-handling-refactor branch from 750bae0 to 521054b Compare February 24, 2018 01:31
A rewrite of error handling, with semantics that more closely match
stack unwinding.

Errors that are thrown during the render phase unwind to the nearest
error boundary, like before. But rather than synchronously unmount the
children before retrying, we restart the failed subtree within the same
render phase. The failed children are still unmounted (as if all their
keys changed) but without an extra commit.

Commit phase errors are different. They work by scheduling an error on
the update queue of the error boundary. When we enter the render phase,
the error is popped off the queue. The rest of the algorithm is
the same.

This approach is designed to work for throwing non-errors, too, though
that feature is not implemented yet.
Fires during the render phase, so you can recover from an error within the same
pass. This aligns error boundaries more closely with try-catch semantics.

Let's keep this behind a feature flag until a future release. For now, the
recommendation is to keep using componentDidCatch. Eventually, the advice will
be to use getDerivedStateFromCatch for handling errors and componentDidCatch
only for logging.
This way we don't have to store the thrown values on the effect list.
We don't need it yet. We'll reconsider once we add another exception type.
This moves it out of the hot path.
@acdlite acdlite force-pushed the error-handling-refactor branch from 521054b to 559bbb5 Compare February 24, 2018 01:37
@acdlite acdlite merged commit 94518b0 into facebook:master Feb 24, 2018
const errorInfo = capturedErrors[i];
const error = errorInfo.value;
logError(finishedWork, errorInfo);
instance.componentDidCatch(error);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did we intentionally "lose" the errorInfo argument here? Seems like a major change.

LeonYuAng3NT pushed a commit to LeonYuAng3NT/react that referenced this pull request Mar 22, 2018
* Add stack unwinding phase for handling errors

A rewrite of error handling, with semantics that more closely match
stack unwinding.

Errors that are thrown during the render phase unwind to the nearest
error boundary, like before. But rather than synchronously unmount the
children before retrying, we restart the failed subtree within the same
render phase. The failed children are still unmounted (as if all their
keys changed) but without an extra commit.

Commit phase errors are different. They work by scheduling an error on
the update queue of the error boundary. When we enter the render phase,
the error is popped off the queue. The rest of the algorithm is
the same.

This approach is designed to work for throwing non-errors, too, though
that feature is not implemented yet.

* Add experimental getDerivedStateFromCatch lifecycle

Fires during the render phase, so you can recover from an error within the same
pass. This aligns error boundaries more closely with try-catch semantics.

Let's keep this behind a feature flag until a future release. For now, the
recommendation is to keep using componentDidCatch. Eventually, the advice will
be to use getDerivedStateFromCatch for handling errors and componentDidCatch
only for logging.

* Reconcile twice to remount failed children, instead of using a boolean

* Handle effect immediately after its thrown

This way we don't have to store the thrown values on the effect list.

* ReactFiberIncompleteWork -> ReactFiberUnwindWork

* Remove startTime

* Remove TypeOfException

We don't need it yet. We'll reconsider once we add another exception type.

* Move replay to outer catch block

This moves it out of the hot path.
rhagigi pushed a commit to rhagigi/react that referenced this pull request Apr 19, 2018
* Add stack unwinding phase for handling errors

A rewrite of error handling, with semantics that more closely match
stack unwinding.

Errors that are thrown during the render phase unwind to the nearest
error boundary, like before. But rather than synchronously unmount the
children before retrying, we restart the failed subtree within the same
render phase. The failed children are still unmounted (as if all their
keys changed) but without an extra commit.

Commit phase errors are different. They work by scheduling an error on
the update queue of the error boundary. When we enter the render phase,
the error is popped off the queue. The rest of the algorithm is
the same.

This approach is designed to work for throwing non-errors, too, though
that feature is not implemented yet.

* Add experimental getDerivedStateFromCatch lifecycle

Fires during the render phase, so you can recover from an error within the same
pass. This aligns error boundaries more closely with try-catch semantics.

Let's keep this behind a feature flag until a future release. For now, the
recommendation is to keep using componentDidCatch. Eventually, the advice will
be to use getDerivedStateFromCatch for handling errors and componentDidCatch
only for logging.

* Reconcile twice to remount failed children, instead of using a boolean

* Handle effect immediately after its thrown

This way we don't have to store the thrown values on the effect list.

* ReactFiberIncompleteWork -> ReactFiberUnwindWork

* Remove startTime

* Remove TypeOfException

We don't need it yet. We'll reconsider once we add another exception type.

* Move replay to outer catch block

This moves it out of the hot path.
@NE-SmallTown
Copy link
Contributor

When we enter the render phase, the error is popped off the queue. The rest of the algorithm is the same.

Should 'render phase' be 'commit phase'?

@@ -1379,6 +1387,10 @@ describe('ReactErrorBoundaries', () => {
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary componentWillUpdate',
'ErrorBoundary componentDidUpdate',
Copy link
Contributor

@NE-SmallTown NE-SmallTown Apr 22, 2018

Choose a reason for hiding this comment

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

@gaearon @acdlite Could you please tell me what does 'The initial render was aborted' mean? I'm a little confused that we call componentWillUpdate and componentDidUpdate but don't call the render method, this is different with the common life cycle process

NMinhNguyen referenced this pull request in enzymejs/react-shallow-renderer Jan 29, 2020
* Add stack unwinding phase for handling errors

A rewrite of error handling, with semantics that more closely match
stack unwinding.

Errors that are thrown during the render phase unwind to the nearest
error boundary, like before. But rather than synchronously unmount the
children before retrying, we restart the failed subtree within the same
render phase. The failed children are still unmounted (as if all their
keys changed) but without an extra commit.

Commit phase errors are different. They work by scheduling an error on
the update queue of the error boundary. When we enter the render phase,
the error is popped off the queue. The rest of the algorithm is
the same.

This approach is designed to work for throwing non-errors, too, though
that feature is not implemented yet.

* Add experimental getDerivedStateFromCatch lifecycle

Fires during the render phase, so you can recover from an error within the same
pass. This aligns error boundaries more closely with try-catch semantics.

Let's keep this behind a feature flag until a future release. For now, the
recommendation is to keep using componentDidCatch. Eventually, the advice will
be to use getDerivedStateFromCatch for handling errors and componentDidCatch
only for logging.

* Reconcile twice to remount failed children, instead of using a boolean

* Handle effect immediately after its thrown

This way we don't have to store the thrown values on the effect list.

* ReactFiberIncompleteWork -> ReactFiberUnwindWork

* Remove startTime

* Remove TypeOfException

We don't need it yet. We'll reconsider once we add another exception type.

* Move replay to outer catch block

This moves it out of the hot path.
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.

6 participants