[Fizz] Track Current debugTask and run it for onError Callbacks (#30182)

Stacked on #30174.

This tracks the current debugTask on the Task so that when an error is
thrown we can use it to run the `onError` (and `onShellError` and
`onFatalError`) callbacks within the Context of that task. Ideally it
would be associated with the error object but neither console.error [nor
reportError](https://crbug.com/350426235) reports this as the async
stack so we have to manually restore it.

That way when you inspect Fizz using node `--inspect` we show the right
async stack.

<img width="616" alt="Screenshot 2024-07-01 at 10 52 29 PM"
src="https://github.com/facebook/react/assets/63648/db68133e-124e-4509-8241-c67160db94fc">

This is equivalent to how we track the task that created the parent
server component or the Fiber:


6d2a97a711/packages/react-reconciler/src/ReactChildFiber.js (L1985)

Then use them when invoking the error callbacks:


6d2a97a711/packages/react-reconciler/src/ReactFiberThrow.js (L104-L108)

---------

Co-authored-by: Sebastian Silbermann <silbermann.sebastian@gmail.com>
This commit is contained in:
Sebastian Markbåge
2024-07-02 19:46:18 -04:00
committed by GitHub
parent cfb8945f51
commit 3db98c9177

View File

@@ -250,6 +250,7 @@ type RenderTask = {
thenableState: null | ThenableState,
isFallback: boolean, // whether this task is rendering inside a fallback tree
legacyContext: LegacyContext, // the current legacy context that this task is executing in
debugTask: null | ConsoleTask, // DEV only
// DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
// Consider splitting into multiple objects or consolidating some fields.
};
@@ -279,6 +280,7 @@ type ReplayTask = {
thenableState: null | ThenableState,
isFallback: boolean, // whether this task is rendering inside a fallback tree
legacyContext: LegacyContext, // the current legacy context that this task is executing in
debugTask: null | ConsoleTask, // DEV only
// DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
// Consider splitting into multiple objects or consolidating some fields.
};
@@ -468,6 +470,7 @@ function RequestInstance(
null,
false,
emptyContextObject,
null,
);
pingedTasks.push(rootTask);
}
@@ -610,6 +613,7 @@ export function resumeRequest(
null,
false,
emptyContextObject,
null,
);
pingedTasks.push(rootTask);
return request;
@@ -636,6 +640,7 @@ export function resumeRequest(
null,
false,
emptyContextObject,
null,
);
pingedTasks.push(rootTask);
return request;
@@ -704,6 +709,7 @@ function createRenderTask(
componentStack: null | ComponentStackNode,
isFallback: boolean,
legacyContext: LegacyContext,
debugTask: null | ConsoleTask,
): RenderTask {
request.allPendingTasks++;
if (blockedBoundary === null) {
@@ -731,6 +737,9 @@ function createRenderTask(
if (!disableLegacyContext) {
task.legacyContext = legacyContext;
}
if (__DEV__ && enableOwnerStacks) {
task.debugTask = debugTask;
}
abortSet.add(task);
return task;
}
@@ -751,6 +760,7 @@ function createReplayTask(
componentStack: null | ComponentStackNode,
isFallback: boolean,
legacyContext: LegacyContext,
debugTask: null | ConsoleTask,
): ReplayTask {
request.allPendingTasks++;
if (blockedBoundary === null) {
@@ -779,6 +789,9 @@ function createReplayTask(
if (!disableLegacyContext) {
task.legacyContext = legacyContext;
}
if (__DEV__ && enableOwnerStacks) {
task.debugTask = debugTask;
}
abortSet.add(task);
return task;
}
@@ -887,40 +900,44 @@ function createClassComponentStack(
type,
};
}
function createServerComponentStack(
function pushServerComponentStack(
task: Task,
debugInfo: void | null | ReactDebugInfo,
): null | ComponentStackNode {
): void {
if (!__DEV__) {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'pushServerComponentStack should never be called in production. This is a bug in React.',
);
}
// Build a Server Component parent stack from the debugInfo.
if (__DEV__) {
let node = task.componentStack;
if (debugInfo != null) {
const stack: ReactDebugInfo = debugInfo;
for (let i = 0; i < stack.length; i++) {
const componentInfo: ReactComponentInfo = (stack[i]: any);
if (typeof componentInfo.name !== 'string') {
continue;
}
let name = componentInfo.name;
const env = componentInfo.env;
if (env) {
name += ' (' + env + ')';
}
node = {
tag: 3,
parent: node,
type: name,
owner: componentInfo.owner,
stack: componentInfo.stack,
};
if (debugInfo != null) {
const stack: ReactDebugInfo = debugInfo;
for (let i = 0; i < stack.length; i++) {
const componentInfo: ReactComponentInfo = (stack[i]: any);
if (typeof componentInfo.name !== 'string') {
continue;
}
if (enableOwnerStacks && componentInfo.stack === undefined) {
continue;
}
let name = componentInfo.name;
const env = componentInfo.env;
if (env) {
name += ' (' + env + ')';
}
task.componentStack = {
tag: 3,
parent: task.componentStack,
type: name,
owner: componentInfo.owner,
stack: componentInfo.stack,
};
if (enableOwnerStacks) {
task.debugTask = (componentInfo.task: any);
}
}
return node;
}
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'createServerComponentStack should never be called in production. This is a bug in React.',
);
}
function createComponentStackFromType(
@@ -1000,20 +1017,31 @@ function logPostpone(
request: Request,
reason: string,
postponeInfo: ThrownInfo,
debugTask: null | ConsoleTask,
): void {
// If this callback errors, we intentionally let that error bubble up to become a fatal error
// so that someone fixes the error reporting instead of hiding it.
request.onPostpone(reason, postponeInfo);
const onPostpone = request.onPostpone;
if (__DEV__ && enableOwnerStacks && debugTask) {
debugTask.run(onPostpone.bind(null, reason, postponeInfo));
} else {
onPostpone(reason, postponeInfo);
}
}
function logRecoverableError(
request: Request,
error: any,
errorInfo: ThrownInfo,
debugTask: null | ConsoleTask,
): ?string {
// If this callback errors, we intentionally let that error bubble up to become a fatal error
// so that someone fixes the error reporting instead of hiding it.
const errorDigest = request.onError(error, errorInfo);
const onError = request.onError;
const errorDigest =
__DEV__ && enableOwnerStacks && debugTask
? debugTask.run(onError.bind(null, error, errorInfo))
: onError(error, errorInfo);
if (errorDigest != null && typeof errorDigest !== 'string') {
// We used to throw here but since this gets called from a variety of unprotected places it
// seems better to just warn and discard the returned value.
@@ -1028,14 +1056,24 @@ function logRecoverableError(
return errorDigest;
}
function fatalError(request: Request, error: mixed): void {
function fatalError(
request: Request,
error: mixed,
errorInfo: ThrownInfo,
debugTask: null | ConsoleTask,
): void {
// This is called outside error handling code such as if the root errors outside
// a suspense boundary or if the root suspense boundary's fallback errors.
// It's also called if React itself or its host configs errors.
const onShellError = request.onShellError;
onShellError(error);
const onFatalError = request.onFatalError;
onFatalError(error);
if (__DEV__ && enableOwnerStacks && debugTask) {
debugTask.run(onShellError.bind(null, error));
debugTask.run(onFatalError.bind(null, error));
} else {
onShellError(error);
onFatalError(error);
}
if (request.destination !== null) {
request.status = CLOSED;
closeWithError(request.destination, error);
@@ -1168,11 +1206,21 @@ function renderSuspenseBoundary(
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message, thrownInfo);
logPostpone(
request,
postponeInstance.message,
thrownInfo,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error, thrownInfo);
errorDigest = logRecoverableError(
request,
error,
thrownInfo,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
}
encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo, false);
@@ -1231,6 +1279,7 @@ function renderSuspenseBoundary(
suspenseComponentStack,
true,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
// TODO: This should be queued at a separate lower priority queue so that we only work
// on preparing fallbacks if we don't have any more main content to task on.
@@ -1314,11 +1363,21 @@ function replaySuspenseBoundary(
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message, thrownInfo);
logPostpone(
request,
postponeInstance.message,
thrownInfo,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error, thrownInfo);
errorDigest = logRecoverableError(
request,
error,
thrownInfo,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
}
encodeErrorForBoundary(
resumedBoundary,
@@ -1371,6 +1430,7 @@ function replaySuspenseBoundary(
suspenseComponentStack,
true,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
// TODO: This should be queued at a separate lower priority queue so that we only work
// on preparing fallbacks if we don't have any more main content to task on.
@@ -2378,6 +2438,7 @@ function replayElement(
thrownInfo,
childNodes,
childSlots,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
}
task.replay = replay;
@@ -2570,12 +2631,17 @@ function renderNodeDestructive(
const owner = __DEV__ ? element._owner : null;
const stack = __DEV__ && enableOwnerStacks ? element._debugStack : null;
let previousDebugTask: null | ConsoleTask = null;
const previousComponentStack = task.componentStack;
let debugTask: null | ConsoleTask;
if (__DEV__) {
task.componentStack = createServerComponentStack(
task,
element._debugInfo,
);
if (enableOwnerStacks) {
previousDebugTask = task.debugTask;
}
pushServerComponentStack(task, element._debugInfo);
if (enableOwnerStacks) {
task.debugTask = debugTask = element._debugTask;
}
}
const name = getComponentNameFromType(type);
@@ -2583,8 +2649,6 @@ function renderNodeDestructive(
key == null ? (childIndex === -1 ? 0 : childIndex) : key;
const keyPath = [task.keyPath, name, keyOrIndex];
if (task.replay !== null) {
const debugTask: null | ConsoleTask =
__DEV__ && enableOwnerStacks ? element._debugTask : null;
if (debugTask) {
debugTask.run(
replayElement.bind(
@@ -2623,8 +2687,6 @@ function renderNodeDestructive(
// prelude and skip it during the replay.
} else {
// We're doing a plain render.
const debugTask: null | ConsoleTask =
__DEV__ && enableOwnerStacks ? element._debugTask : null;
if (debugTask) {
debugTask.run(
renderElement.bind(
@@ -2654,6 +2716,9 @@ function renderNodeDestructive(
}
if (__DEV__) {
task.componentStack = previousComponentStack;
if (enableOwnerStacks) {
task.debugTask = previousDebugTask;
}
}
return;
}
@@ -2665,11 +2730,12 @@ function renderNodeDestructive(
case REACT_LAZY_TYPE: {
const lazyNode: LazyComponentType<any, any> = (node: any);
const previousComponentStack = task.componentStack;
let previousDebugTask = null;
if (__DEV__) {
task.componentStack = createServerComponentStack(
task,
lazyNode._debugInfo,
);
if (enableOwnerStacks) {
previousDebugTask = task.debugTask;
}
pushServerComponentStack(task, lazyNode._debugInfo);
}
if (!__DEV__ || task.componentStack === previousComponentStack) {
// TODO: Do we really need this stack frame? We don't on the client.
@@ -2692,6 +2758,9 @@ function renderNodeDestructive(
// We restore the stack before rendering the resolved node because once the Lazy
// has resolved any future errors
task.componentStack = previousComponentStack;
if (__DEV__ && enableOwnerStacks) {
task.debugTask = previousDebugTask;
}
// Now we render the resolved node
renderNodeDestructive(request, task, resolvedNode, childIndex);
@@ -2812,10 +2881,7 @@ function renderNodeDestructive(
const thenable: Thenable<ReactNodeList> = (maybeUsable: any);
const previousComponentStack = task.componentStack;
if (__DEV__) {
task.componentStack = createServerComponentStack(
task,
thenable._debugInfo,
);
pushServerComponentStack(task, thenable._debugInfo);
}
const result = renderNodeDestructive(
request,
@@ -2947,6 +3013,7 @@ function replayFragment(
thrownInfo,
childNodes,
childSlots,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
}
task.replay = replay;
@@ -3058,13 +3125,14 @@ function renderChildrenArray(
): void {
const prevKeyPath = task.keyPath;
const previousComponentStack = task.componentStack;
let previousDebugTask = null;
if (__DEV__) {
if (enableOwnerStacks) {
previousDebugTask = task.debugTask;
}
// We read debugInfo from task.node instead of children because it might have been an
// unwrapped iterable so we read from the original node.
task.componentStack = createServerComponentStack(
task,
(task.node: any)._debugInfo,
);
pushServerComponentStack(task, (task.node: any)._debugInfo);
}
if (childIndex !== -1) {
task.keyPath = [task.keyPath, 'Fragment', childIndex];
@@ -3079,6 +3147,9 @@ function renderChildrenArray(
task.keyPath = prevKeyPath;
if (__DEV__) {
task.componentStack = previousComponentStack;
if (enableOwnerStacks) {
task.debugTask = previousDebugTask;
}
}
return;
}
@@ -3112,6 +3183,9 @@ function renderChildrenArray(
task.keyPath = prevKeyPath;
if (__DEV__) {
task.componentStack = previousComponentStack;
if (enableOwnerStacks) {
task.debugTask = previousDebugTask;
}
}
return;
}
@@ -3134,6 +3208,9 @@ function renderChildrenArray(
task.keyPath = prevKeyPath;
if (__DEV__) {
task.componentStack = previousComponentStack;
if (enableOwnerStacks) {
task.debugTask = previousDebugTask;
}
}
}
@@ -3327,7 +3404,12 @@ function injectPostponedHole(
reason: string,
thrownInfo: ThrownInfo,
): Segment {
logPostpone(request, reason, thrownInfo);
logPostpone(
request,
reason,
thrownInfo,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
// Something suspended, we'll need to create a new segment and resolve it later.
const segment = task.blockedSegment;
const insertionIndex = segment.chunks.length;
@@ -3371,6 +3453,7 @@ function spawnNewSuspendedReplayTask(
task.componentStack !== null ? task.componentStack.parent : null,
task.isFallback,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
const ping = newTask.ping;
@@ -3417,6 +3500,7 @@ function spawnNewSuspendedRenderTask(
task.componentStack !== null ? task.componentStack.parent : null,
task.isFallback,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
const ping = newTask.ping;
@@ -3609,6 +3693,7 @@ function erroredReplay(
errorInfo: ThrownInfo,
replayNodes: ReplayNode[],
resumeSlots: ResumeSlots,
debugTask: null | ConsoleTask,
): void {
// Erroring during a replay doesn't actually cause an error by itself because
// that component has already rendered. What causes the error is the resumable
@@ -3625,11 +3710,11 @@ function erroredReplay(
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message, errorInfo);
logPostpone(request, postponeInstance.message, errorInfo, debugTask);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error, errorInfo);
errorDigest = logRecoverableError(request, error, errorInfo, debugTask);
}
abortRemainingReplayNodes(
request,
@@ -3648,6 +3733,7 @@ function erroredTask(
boundary: Root | SuspenseBoundary,
error: mixed,
errorInfo: ThrownInfo,
debugTask: null | ConsoleTask,
) {
// Report the error to a global handler.
let errorDigest;
@@ -3658,14 +3744,14 @@ function erroredTask(
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message, errorInfo);
logPostpone(request, postponeInstance.message, errorInfo, debugTask);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error, errorInfo);
errorDigest = logRecoverableError(request, error, errorInfo, debugTask);
}
if (boundary === null) {
fatalError(request, error);
fatalError(request, error, errorInfo, debugTask);
} else {
boundary.pendingTasks--;
if (boundary.status !== CLIENT_RENDERED) {
@@ -3821,11 +3907,11 @@ function abortTask(task: Task, request: Request, error: mixed): void {
'The render was aborted with postpone when the shell is incomplete. Reason: ' +
postponeInstance.message,
);
logRecoverableError(request, fatal, errorInfo);
fatalError(request, fatal);
logRecoverableError(request, fatal, errorInfo, null);
fatalError(request, fatal, errorInfo, null);
} else {
logRecoverableError(request, error, errorInfo);
fatalError(request, error);
logRecoverableError(request, error, errorInfo, null);
fatalError(request, error, errorInfo, null);
}
return;
} else {
@@ -3842,11 +3928,11 @@ function abortTask(task: Task, request: Request, error: mixed): void {
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message, errorInfo);
logPostpone(request, postponeInstance.message, errorInfo, null);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error, errorInfo);
errorDigest = logRecoverableError(request, error, errorInfo, null);
}
abortRemainingReplayNodes(
request,
@@ -3880,11 +3966,11 @@ function abortTask(task: Task, request: Request, error: mixed): void {
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message, errorInfo);
logPostpone(request, postponeInstance.message, errorInfo, null);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error, errorInfo);
errorDigest = logRecoverableError(request, error, errorInfo, null);
}
encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true);
@@ -3922,7 +4008,7 @@ function safelyEmitEarlyPreloads(
} catch (error) {
// We assume preloads are optimistic and thus non-fatal if errored.
const errorInfo: ThrownInfo = {};
logRecoverableError(request, error, errorInfo);
logRecoverableError(request, error, errorInfo, null);
}
}
@@ -4164,7 +4250,12 @@ function retryRenderTask(
const postponeInstance: Postpone = (x: any);
const postponeInfo = getThrownInfo(task.componentStack);
logPostpone(request, postponeInstance.message, postponeInfo);
logPostpone(
request,
postponeInstance.message,
postponeInfo,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
trackPostpone(request, trackedPostpones, task, segment);
finishedTask(request, task.blockedBoundary, segment);
return;
@@ -4174,7 +4265,13 @@ function retryRenderTask(
const errorInfo = getThrownInfo(task.componentStack);
task.abortSet.delete(task);
segment.status = ERRORED;
erroredTask(request, task.blockedBoundary, x, errorInfo);
erroredTask(
request,
task.blockedBoundary,
x,
errorInfo,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
return;
} finally {
if (__DEV__) {
@@ -4253,6 +4350,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
errorInfo,
task.replay.nodes,
task.replay.slots,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
request.pendingRootTasks--;
if (request.pendingRootTasks === 0) {
@@ -4306,8 +4404,8 @@ export function performWork(request: Request): void {
}
} catch (error) {
const errorInfo: ThrownInfo = {};
logRecoverableError(request, error, errorInfo);
fatalError(request, error);
logRecoverableError(request, error, errorInfo, null);
fatalError(request, error, errorInfo, null);
} finally {
setCurrentResumableState(prevResumableState);
ReactSharedInternals.H = prevDispatcher;
@@ -4891,8 +4989,8 @@ export function startFlowing(request: Request, destination: Destination): void {
flushCompletedQueues(request, destination);
} catch (error) {
const errorInfo: ThrownInfo = {};
logRecoverableError(request, error, errorInfo);
fatalError(request, error);
logRecoverableError(request, error, errorInfo, null);
fatalError(request, error, errorInfo, null);
}
}
@@ -4917,8 +5015,8 @@ export function abort(request: Request, reason: mixed): void {
}
} catch (error) {
const errorInfo: ThrownInfo = {};
logRecoverableError(request, error, errorInfo);
fatalError(request, error);
logRecoverableError(request, error, errorInfo, null);
fatalError(request, error, errorInfo, null);
}
}