Skip to content

Commit 0b85dcb

Browse files
committed
Create errors lazily so that we can mutate their messages
1 parent 6404416 commit 0b85dcb

File tree

4 files changed

+290
-56
lines changed

4 files changed

+290
-56
lines changed

packages/next/src/server/app-render/app-render.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4132,14 +4132,14 @@ async function validateInstantConfigs(
41324132
: false
41334133
})
41344134

4135-
// Collect the first debug stack from segments with instant configs so we can
4136-
// pass it into validation state for setting `cause` on errors.
4137-
const debugInstantStack =
4135+
// Collect the first stack factory from segments with instant configs so we can
4136+
// pass it into validation state for creating errors with the config's stack trace.
4137+
const createInstantStack =
41384138
segmentsWithInstantConfigs
41394139
.map(
4140-
(segmentPath) => treeNodes.get(segmentPath)?.module?.debugInstantStack
4140+
(segmentPath) => treeNodes.get(segmentPath)?.module?.createInstantStack
41414141
)
4142-
.find((stack): stack is Error => stack != null) ?? null
4142+
.find((factory): factory is () => Error => factory != null) ?? null
41434143

41444144
const clientReferenceManifest = getClientReferenceManifest()
41454145

@@ -4181,7 +4181,7 @@ async function validateInstantConfigs(
41814181
validationRouteTree,
41824182
navigationParent,
41834183
false, // use static stage for static segments
4184-
debugInstantStack
4184+
createInstantStack
41854185
)
41864186
if (initialResults.errors.length === 0) {
41874187
debug?.(` ✅ Validation successful`)
@@ -4201,7 +4201,7 @@ async function validateInstantConfigs(
42014201
validationRouteTree,
42024202
navigationParent,
42034203
true, // use runtime stage for static segments instead
4204-
debugInstantStack
4204+
createInstantStack
42054205
)
42064206
if (runtimeResults.errors.length > 0) {
42074207
// The errors remained in the runtime stage, so they were caused by a dynamic access.
@@ -4234,7 +4234,7 @@ async function validateInstantConfigNavigation(
42344234
routeTree: InstantValidation.RouteTree,
42354235
navigationParent: InstantValidation.SegmentPath,
42364236
useRuntimeStageForPartialSegments: boolean,
4237-
debugInstantStack: Error | null
4237+
createInstantStack: (() => Error) | null
42384238
): Promise<{ dynamicHoleKind: DynamicHoleKind; errors: Array<unknown> }> {
42394239
const { implicitTags, nonce, workStore } = ctx
42404240
const isDebugChannelEnabled = !!ctx.renderOpts.setReactDebugChannel
@@ -4250,7 +4250,7 @@ async function validateInstantConfigNavigation(
42504250
const preinitScripts = () => {}
42514251
const { ServerInsertedHTMLProvider } = createServerInsertedHTML()
42524252

4253-
const dynamicValidation = createInstantValidationState(debugInstantStack)
4253+
const dynamicValidation = createInstantValidationState(createInstantStack)
42544254
const boundaryState = createValidationBoundaryTracking()
42554255

42564256
const finalClientPrerenderStore: PrerenderStore = {

packages/next/src/server/app-render/dynamic-rendering.ts

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -796,11 +796,11 @@ export type InstantValidationState = {
796796
dynamicErrors: Array<Error>
797797
validationPreventingErrors: Array<Error>
798798
thrownErrorsOutsideBoundary: Array<unknown>
799-
debugInstantStack: Error | null
799+
createInstantStack: (() => Error) | null
800800
}
801801

802802
export function createInstantValidationState(
803-
debugInstantStack: Error | null
803+
createInstantStack: (() => Error) | null
804804
): InstantValidationState {
805805
return {
806806
hasDynamicMetadata: false,
@@ -811,7 +811,7 @@ export function createInstantValidationState(
811811
dynamicErrors: [],
812812
validationPreventingErrors: [],
813813
thrownErrorsOutsideBoundary: [],
814-
debugInstantStack,
814+
createInstantStack,
815815
}
816816
}
817817

@@ -836,7 +836,7 @@ export function trackDynamicHoleInNavigation(
836836
const error = createErrorWithComponentOrOwnerStack(
837837
message,
838838
componentStack,
839-
dynamicValidation.debugInstantStack
839+
dynamicValidation.createInstantStack
840840
)
841841
dynamicValidation.dynamicMetadata = error
842842
return
@@ -850,7 +850,7 @@ export function trackDynamicHoleInNavigation(
850850
const error = createErrorWithComponentOrOwnerStack(
851851
message,
852852
componentStack,
853-
dynamicValidation.debugInstantStack
853+
dynamicValidation.createInstantStack
854854
)
855855
dynamicValidation.dynamicErrors.push(error)
856856
return
@@ -880,7 +880,7 @@ export function trackDynamicHoleInNavigation(
880880
const error = createErrorWithComponentOrOwnerStack(
881881
message,
882882
componentStack,
883-
dynamicValidation.debugInstantStack
883+
dynamicValidation.createInstantStack
884884
)
885885
dynamicValidation.validationPreventingErrors.push(error)
886886
return
@@ -920,8 +920,11 @@ export function trackDynamicHoleInNavigation(
920920
if (clientDynamic.syncDynamicErrorWithStack) {
921921
// This task was the task that called the sync error.
922922
const syncError = clientDynamic.syncDynamicErrorWithStack
923-
if (dynamicValidation.debugInstantStack && syncError.cause === undefined) {
924-
syncError.cause = dynamicValidation.debugInstantStack
923+
if (
924+
dynamicValidation.createInstantStack !== null &&
925+
syncError.cause === undefined
926+
) {
927+
syncError.cause = dynamicValidation.createInstantStack()
925928
}
926929
dynamicValidation.dynamicErrors.push(syncError)
927930
return
@@ -935,7 +938,7 @@ export function trackDynamicHoleInNavigation(
935938
const error = createErrorWithComponentOrOwnerStack(
936939
message,
937940
componentStack,
938-
dynamicValidation.debugInstantStack
941+
dynamicValidation.createInstantStack
939942
)
940943
dynamicValidation.dynamicErrors.push(error)
941944
return
@@ -1083,14 +1086,15 @@ export function trackDynamicHoleInStaticShell(
10831086
function createErrorWithComponentOrOwnerStack(
10841087
message: string,
10851088
componentStack: string,
1086-
cause: Error | null
1089+
createInstantStack: (() => Error) | null
10871090
) {
10881091
const ownerStack =
10891092
process.env.NODE_ENV !== 'production' && React.captureOwnerStack
10901093
? React.captureOwnerStack()
10911094
: null
10921095

1093-
const error = new Error(message, cause !== null ? { cause } : {})
1096+
const cause = createInstantStack !== null ? createInstantStack() : null
1097+
const error = new Error(message, cause !== null ? { cause } : undefined)
10941098
// TODO go back to owner stack here if available. This is temporarily using componentStack to get the right
10951099
//
10961100
error.stack = error.name + ': ' + message + (ownerStack || componentStack)
@@ -1245,31 +1249,29 @@ export function getNavigationDisallowedDynamicReasons(
12451249
}
12461250

12471251
if (boundaryState.renderedIds.size < boundaryState.expectedIds.size) {
1248-
const { thrownErrorsOutsideBoundary, debugInstantStack } = dynamicValidation
1249-
const causeOption = debugInstantStack ? { cause: debugInstantStack } : {}
1252+
const { thrownErrorsOutsideBoundary, createInstantStack } =
1253+
dynamicValidation
12501254
if (thrownErrorsOutsideBoundary.length === 0) {
1251-
return [
1252-
new Error(
1253-
`Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.`,
1254-
causeOption
1255-
),
1256-
]
1255+
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.`
1256+
const error =
1257+
createInstantStack !== null ? createInstantStack() : new Error()
1258+
error.name = 'Error'
1259+
error.message = message
1260+
return [error]
12571261
} else if (thrownErrorsOutsideBoundary.length === 1) {
1258-
return [
1259-
new Error(
1260-
`Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.`,
1261-
causeOption
1262-
),
1263-
thrownErrorsOutsideBoundary[0] as Error,
1264-
]
1262+
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.`
1263+
const error =
1264+
createInstantStack !== null ? createInstantStack() : new Error()
1265+
error.name = 'Error'
1266+
error.message = message
1267+
return [error, thrownErrorsOutsideBoundary[0] as Error]
12651268
} else {
1266-
return [
1267-
new Error(
1268-
`Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to one of the following errors.`,
1269-
causeOption
1270-
),
1271-
...(thrownErrorsOutsideBoundary as Error[]),
1272-
]
1269+
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to one of the following errors.`
1270+
const error =
1271+
createInstantStack !== null ? createInstantStack() : new Error()
1272+
error.name = 'Error'
1273+
error.message = message
1274+
return [error, ...(thrownErrorsOutsideBoundary as Error[])]
12731275
}
12741276
}
12751277

packages/next/src/server/app-render/instant-validation/instant-validation.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export type RouteTree = {
8686
// TODO(instant-validation): We should know if a layout segment is shared
8787
instantConfig: InstantConfig | null
8888
conventionPath: string
89-
debugInstantStack: Error | null
89+
createInstantStack: (() => Error) | null
9090
}
9191

9292
slots: { [parallelRouteKey: string]: RouteTree } | null
@@ -144,17 +144,17 @@ export async function findNavigationsToValidate(
144144
// TODO(restart-on-cache-miss): Does this work correctly for client page/layout modules?
145145
const instantConfig =
146146
(layoutOrPageMod as AppSegmentConfig).unstable_instant ?? null
147-
const createInstantConfigStack: unknown = (layoutOrPageMod as any)
147+
const rawFactory: unknown = (layoutOrPageMod as any)
148148
.__debugCreateInstantConfigStack
149-
const debugInstantStack: Error | null =
150-
typeof createInstantConfigStack === 'function'
151-
? createInstantConfigStack()
149+
const createInstantStack: (() => Error) | null =
150+
typeof rawFactory === 'function'
151+
? (rawFactory as () => Error)
152152
: null
153153
moduleInfo = {
154154
type: modType!,
155155
instantConfig,
156156
conventionPath: conventionPath!,
157-
debugInstantStack,
157+
createInstantStack,
158158
}
159159

160160
if (isInsideParallelSlot) {
@@ -179,10 +179,13 @@ export async function findNavigationsToValidate(
179179
} else {
180180
const isRootLayout = parentLayoutPath === null
181181
if (isRootLayout && instantConfig.prefetch === 'runtime') {
182-
throw new Error(
183-
`${conventionPath}: \`unstable_instant\` with mode 'runtime' is not supported in root layouts.`,
184-
debugInstantStack ? { cause: debugInstantStack } : {}
185-
)
182+
const message = `${conventionPath}: \`unstable_instant\` with mode 'runtime' is not supported in root layouts.`
183+
const error =
184+
createInstantStack !== null
185+
? createInstantStack()
186+
: new Error()
187+
error.message = message
188+
throw error
186189
}
187190

188191
const task: ValidationTask = {

0 commit comments

Comments
 (0)