-
Notifications
You must be signed in to change notification settings - Fork 47.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
stash the component stack on the thrown value and reuse (#25790)
ErrorBoundaries are currently not fully composable. The reason is if you decide your boundary cannot handle a particular error and rethrow it to higher boundary the React runtime does not understand that this throw is a forward and it recreates the component stack from the Boundary position. This loses fidelity and is especially bad if the boundary is limited it what it handles and high up in the component tree. This implementation uses a WeakMap to store component stacks for values that are objects. If an error is rethrown from an ErrorBoundary the stack will be pulled from the map if it exists. This doesn't work for thrown primitives but this is uncommon and stashing the stack on the primitive also wouldn't work
- Loading branch information
Showing
3 changed files
with
129 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @emails react-core | ||
* @jest-environment node | ||
*/ | ||
'use strict'; | ||
|
||
let React; | ||
let ReactNoop; | ||
let waitForAll; | ||
|
||
describe('ReactFragment', () => { | ||
beforeEach(function () { | ||
jest.resetModules(); | ||
|
||
React = require('react'); | ||
ReactNoop = require('react-noop-renderer'); | ||
const InternalTestUtils = require('internal-test-utils'); | ||
waitForAll = InternalTestUtils.waitForAll; | ||
}); | ||
|
||
function componentStack(components) { | ||
return components | ||
.map(component => `\n in ${component} (at **)`) | ||
.join(''); | ||
} | ||
|
||
function normalizeCodeLocInfo(str) { | ||
return ( | ||
str && | ||
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { | ||
return '\n in ' + name + ' (at **)'; | ||
}) | ||
); | ||
} | ||
|
||
it('retains component stacks when rethrowing an error', async () => { | ||
function Foo() { | ||
return ( | ||
<RethrowingBoundary> | ||
<Bar /> | ||
</RethrowingBoundary> | ||
); | ||
} | ||
function Bar() { | ||
return <SomethingThatErrors />; | ||
} | ||
function SomethingThatErrors() { | ||
throw new Error('uh oh'); | ||
} | ||
|
||
class RethrowingBoundary extends React.Component { | ||
static getDerivedStateFromError(error) { | ||
throw error; | ||
} | ||
|
||
render() { | ||
return this.props.children; | ||
} | ||
} | ||
|
||
const errors = []; | ||
class CatchingBoundary extends React.Component { | ||
constructor() { | ||
super(); | ||
this.state = {}; | ||
} | ||
static getDerivedStateFromError(error) { | ||
return {errored: true}; | ||
} | ||
componentDidCatch(err, errInfo) { | ||
errors.push(err.message, normalizeCodeLocInfo(errInfo.componentStack)); | ||
} | ||
render() { | ||
if (this.state.errored) { | ||
return null; | ||
} | ||
return this.props.children; | ||
} | ||
} | ||
|
||
ReactNoop.render( | ||
<CatchingBoundary> | ||
<Foo /> | ||
</CatchingBoundary>, | ||
); | ||
await waitForAll([]); | ||
expect(errors).toEqual([ | ||
'uh oh', | ||
componentStack([ | ||
'SomethingThatErrors', | ||
'Bar', | ||
'RethrowingBoundary', | ||
'Foo', | ||
'CatchingBoundary', | ||
]), | ||
]); | ||
}); | ||
}); |
react/packages/react-reconciler/src/ReactFiberWorkLoop.js
Line 294 in a9cc325
We use this defensive pattern elsewhere since WeakMap took a while to be widely supported and it would error early. We can probably start removing that defensive pattern though.