Skip to content

Commit

Permalink
stash the component stack on the thrown value and reuse (#25790)
Browse files Browse the repository at this point in the history
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

DiffTrain build for commit a9cc325.
  • Loading branch information
gnoff committed Feb 16, 2024
1 parent d54257a commit 2a45e92
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<319dc1150d695a3c1932887c45a69d77>>
* @generated SignedSource<<2bd76364e49975f7447df4d717144b25>>
*/

"use strict";
Expand Down Expand Up @@ -11931,17 +11931,37 @@ if (__DEV__) {
return shouldUpdate;
}

var CapturedStacks = new WeakMap();
function createCapturedValueAtFiber(value, source) {
// If the value is an error, call this function immediately after it is thrown
// so the stack is accurate.
var stack;

if (typeof value === "object" && value !== null) {
var capturedStack = CapturedStacks.get(value);

if (typeof capturedStack === "string") {
stack = capturedStack;
} else {
stack = getStackByFiberInDevAndProd(source);
CapturedStacks.set(value, stack);
}
} else {
stack = getStackByFiberInDevAndProd(source);
}

return {
value: value,
source: source,
stack: getStackByFiberInDevAndProd(source),
stack: stack,
digest: null
};
}
function createCapturedValue(value, digest, stack) {
function createCapturedValueFromError(value, digest, stack) {
if (typeof stack === "string") {
CapturedStacks.set(value, stack);
}

return {
value: value,
source: null,
Expand Down Expand Up @@ -14493,7 +14513,7 @@ if (__DEV__) {
}

error.digest = digest;
capturedValue = createCapturedValue(error, digest, stack);
capturedValue = createCapturedValueFromError(error, digest, stack);
}

return retrySuspenseComponentWithoutHydrating(
Expand Down Expand Up @@ -14603,7 +14623,7 @@ if (__DEV__) {
pushPrimaryTreeSuspenseHandler(workInProgress);
workInProgress.flags &= ~ForceClientRender;

var _capturedValue = createCapturedValue(
var _capturedValue = createCapturedValueFromError(
new Error(
"There was an error while hydrating this Suspense boundary. " +
"Switched to client rendering."
Expand Down Expand Up @@ -25613,7 +25633,7 @@ if (__DEV__) {
return root;
}

var ReactVersion = "18.3.0-canary-2ba1b7856-20240215";
var ReactVersion = "18.3.0-canary-a9cc32511-20240215";

// Might add PROFILE later.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<9e3687f63a84b28f925412a2315ed394>>
* @generated SignedSource<<e0eb54caae0b3e65831e66b40e94cf62>>
*/

"use strict";
Expand Down Expand Up @@ -1351,6 +1351,18 @@ function describeFiber(fiber) {
return "";
}
}
function getStackByFiberInDevAndProd(workInProgress) {
try {
var info = "";
do
(info += describeFiber(workInProgress)),
(workInProgress = workInProgress.return);
while (workInProgress);
return info;
} catch (x) {
return "\nError generating stack: " + x.message + "\n" + x.stack;
}
}
var SuspenseException = Error(
"Suspense Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render. You must either rethrow it immediately, or move the `use` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.\n\nTo handle async errors, wrap your component in an error boundary, or call the promise's `.catch` method and pass the result to `use`"
),
Expand Down Expand Up @@ -3557,22 +3569,23 @@ function mountClassInstance(workInProgress, ctor, newProps, renderLanes) {
"function" === typeof instance.componentDidMount &&
(workInProgress.flags |= 4194308);
}
var CapturedStacks = new WeakMap();
function createCapturedValueAtFiber(value, source) {
try {
var info = "",
node = source;
do (info += describeFiber(node)), (node = node.return);
while (node);
var JSCompiler_inline_result = info;
} catch (x) {
JSCompiler_inline_result =
"\nError generating stack: " + x.message + "\n" + x.stack;
}
if ("object" === typeof value && null !== value) {
var stack = CapturedStacks.get(value);
"string" !== typeof stack &&
((stack = getStackByFiberInDevAndProd(source)),
CapturedStacks.set(value, stack));
} else stack = getStackByFiberInDevAndProd(source);
return { value: value, source: source, stack: stack, digest: null };
}
function createCapturedValueFromError(value, digest, stack) {
"string" === typeof stack && CapturedStacks.set(value, stack);
return {
value: value,
source: source,
stack: JSCompiler_inline_result,
digest: null
source: null,
stack: null != stack ? stack : null,
digest: null != digest ? digest : null
};
}
function logCapturedError(boundary, errorInfo) {
Expand Down Expand Up @@ -4492,18 +4505,16 @@ function updateDehydratedSuspenseComponent(
return (
pushPrimaryTreeSuspenseHandler(workInProgress),
(workInProgress.flags &= -257),
(didPrimaryChildrenDefer = createCapturedValueFromError(
Error(
"There was an error while hydrating this Suspense boundary. Switched to client rendering."
)
)),
retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
{
value: Error(
"There was an error while hydrating this Suspense boundary. Switched to client rendering."
),
source: null,
stack: null,
digest: null
}
didPrimaryChildrenDefer
)
);
if (null !== workInProgress.memoizedState)
Expand Down Expand Up @@ -4560,17 +4571,16 @@ function updateDehydratedSuspenseComponent(
"The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering."
)),
(suspenseState.digest = didPrimaryChildrenDefer),
(didPrimaryChildrenDefer = createCapturedValueFromError(
suspenseState,
didPrimaryChildrenDefer,
void 0
)),
retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
{
value: suspenseState,
source: null,
stack: null,
digest:
null != didPrimaryChildrenDefer ? didPrimaryChildrenDefer : null
}
didPrimaryChildrenDefer
)
);
didPrimaryChildrenDefer = 0 !== (renderLanes & current.childLanes);
Expand Down Expand Up @@ -9164,19 +9174,19 @@ function wrapFiber(fiber) {
fiberToWrapper.set(fiber, wrapper));
return wrapper;
}
var devToolsConfig$jscomp$inline_1024 = {
var devToolsConfig$jscomp$inline_1018 = {
findFiberByHostInstance: function () {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "18.3.0-canary-2ba1b7856-20240215",
version: "18.3.0-canary-a9cc32511-20240215",
rendererPackageName: "react-test-renderer"
};
var internals$jscomp$inline_1205 = {
bundleType: devToolsConfig$jscomp$inline_1024.bundleType,
version: devToolsConfig$jscomp$inline_1024.version,
rendererPackageName: devToolsConfig$jscomp$inline_1024.rendererPackageName,
rendererConfig: devToolsConfig$jscomp$inline_1024.rendererConfig,
var internals$jscomp$inline_1199 = {
bundleType: devToolsConfig$jscomp$inline_1018.bundleType,
version: devToolsConfig$jscomp$inline_1018.version,
rendererPackageName: devToolsConfig$jscomp$inline_1018.rendererPackageName,
rendererConfig: devToolsConfig$jscomp$inline_1018.rendererConfig,
overrideHookState: null,
overrideHookStateDeletePath: null,
overrideHookStateRenamePath: null,
Expand All @@ -9193,26 +9203,26 @@ var internals$jscomp$inline_1205 = {
return null === fiber ? null : fiber.stateNode;
},
findFiberByHostInstance:
devToolsConfig$jscomp$inline_1024.findFiberByHostInstance ||
devToolsConfig$jscomp$inline_1018.findFiberByHostInstance ||
emptyFindFiberByHostInstance,
findHostInstancesForRefresh: null,
scheduleRefresh: null,
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-canary-2ba1b7856-20240215"
reconcilerVersion: "18.3.0-canary-a9cc32511-20240215"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1206 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
var hook$jscomp$inline_1200 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
if (
!hook$jscomp$inline_1206.isDisabled &&
hook$jscomp$inline_1206.supportsFiber
!hook$jscomp$inline_1200.isDisabled &&
hook$jscomp$inline_1200.supportsFiber
)
try {
(rendererID = hook$jscomp$inline_1206.inject(
internals$jscomp$inline_1205
(rendererID = hook$jscomp$inline_1200.inject(
internals$jscomp$inline_1199
)),
(injectedHook = hook$jscomp$inline_1206);
(injectedHook = hook$jscomp$inline_1200);
} catch (err) {}
}
exports._Scheduler = Scheduler;
Expand Down
Loading

0 comments on commit 2a45e92

Please sign in to comment.