Skip to content

Commit 52c8224

Browse files
acdliteeps1lon
authored andcommitted
Update blocking prerender error message
1 parent 0364b65 commit 52c8224

File tree

15 files changed

+434
-110
lines changed

15 files changed

+434
-110
lines changed

packages/next/src/next-devtools/dev-overlay/components/errors/error-message/error-message.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
11
import { useState, useRef, useLayoutEffect } from 'react'
2+
import type { ErrorType } from '../error-type-label/error-type-label'
23

34
export type ErrorMessageType = React.ReactNode
45

56
type ErrorMessageProps = {
67
errorMessage: ErrorMessageType
8+
errorType: ErrorType
79
}
810

9-
export function ErrorMessage({ errorMessage }: ErrorMessageProps) {
11+
export function ErrorMessage({ errorMessage, errorType }: ErrorMessageProps) {
1012
const [isExpanded, setIsExpanded] = useState(false)
11-
const [shouldTruncate, setShouldTruncate] = useState(false)
12-
const messageRef = useRef<HTMLParagraphElement>(null)
13+
const [isTooTall, setIsTooTall] = useState(false)
14+
const messageRef = useRef<HTMLDivElement>(null)
1315

1416
useLayoutEffect(() => {
1517
if (messageRef.current) {
16-
setShouldTruncate(messageRef.current.scrollHeight > 200)
18+
setIsTooTall(messageRef.current.scrollHeight > 200)
1719
}
1820
}, [errorMessage])
1921

22+
// The "Blocking Route" error message is specifically formatted to look nice
23+
// in the overlay (rather than just passed through from the console), so we
24+
// intentionally don't truncate it and rely on the scroll overflow instead.
25+
const shouldTruncate = isTooTall && errorType !== 'Blocking Route'
26+
2027
return (
2128
<div className="nextjs__container_errors_wrapper">
22-
<p
29+
<div
2330
ref={messageRef}
2431
id="nextjs__container_errors_desc"
2532
className={`nextjs__container_errors_desc ${shouldTruncate && !isExpanded ? 'truncated' : ''}`}
2633
>
2734
{errorMessage}
28-
</p>
35+
</div>
2936
{shouldTruncate && !isExpanded && (
3037
<>
3138
<div className="nextjs__container_errors_gradient_overlay" />

packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-layout/error-overlay-layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ export function ErrorOverlayLayout({
158158
generateErrorInfo={generateErrorInfo}
159159
/>
160160
</div>
161-
<ErrorMessage errorMessage={errorMessage} />
161+
<ErrorMessage
162+
errorMessage={errorMessage}
163+
errorType={errorType}
164+
/>
162165
</ErrorOverlayDialogHeader>
163166

164167
<ErrorOverlayDialogBody>{children}</ErrorOverlayDialogBody>

packages/next/src/next-devtools/dev-overlay/components/errors/error-type-label/error-type-label.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export type ErrorType =
33
| `Runtime ${string}`
44
| `Console ${string}`
55
| `Recoverable ${string}`
6+
| 'Blocking Route'
67

78
type ErrorTypeLabelProps = {
89
errorType: ErrorType
@@ -12,7 +13,7 @@ export function ErrorTypeLabel({ errorType }: ErrorTypeLabelProps) {
1213
return (
1314
<span
1415
id="nextjs__container_errors_label"
15-
className="nextjs__container_errors_label"
16+
className={`nextjs__container_errors_label ${errorType === 'Blocking Route' ? 'nextjs__container_errors_label_blocking_page' : ''}`}
1617
>
1718
{errorType}
1819
</span>
@@ -31,4 +32,9 @@ export const styles = `
3132
font-family: var(--font-stack-monospace);
3233
line-height: var(--size-20);
3334
}
35+
36+
.nextjs__container_errors_label_blocking_page {
37+
background: var(--color-blue-100);
38+
color: var(--color-blue-900);
39+
}
3440
`

packages/next/src/next-devtools/dev-overlay/container/errors.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,43 @@ function GenericErrorDescription({ error }: { error: Error }) {
5656
)
5757
}
5858

59+
function BlockingPageLoadErrorDescription() {
60+
return (
61+
<div className="nextjs__blocking_page_load_error_description">
62+
<h3 className="nextjs__blocking_page_load_error_description_title">
63+
Uncached data was accessed outside of {'<Suspense>'}
64+
</h3>
65+
<p>
66+
This delays the entire page from rendering, resulting in a slow user
67+
experience. Next.js uses this error to ensure your app loads instantly
68+
on every navigation.
69+
</p>
70+
<p>To fix this, you can:</p>
71+
<ul>
72+
<li>
73+
Wrap the component in a {'<Suspense>'} boundary. This allows Next.js
74+
to stream its contents to the user as soon as it's ready, without
75+
blocking the rest of the app.
76+
</li>
77+
<li>
78+
Move the asynchronous await into a Cache Component (
79+
<code>"use cache"</code>). This allows Next.js to statically prerender
80+
the component as part of the HTML document, so it's instantly visible
81+
to the user. Note that request-specific information &mdash; such as
82+
params, cookies, and headers &mdash; is not available during static
83+
prerendering.
84+
</li>
85+
</ul>
86+
<p>
87+
Learn more:{' '}
88+
<a href="https://nextjs.org/docs/messages/blocking-route">
89+
https://nextjs.org/docs/messages/blocking-route
90+
</a>
91+
</p>
92+
</div>
93+
)
94+
}
95+
5996
export function getErrorTypeLabel(
6097
error: Error,
6198
type: ReadyRuntimeError['type']
@@ -64,6 +101,12 @@ export function getErrorTypeLabel(
64101
return `Recoverable ${error.name}`
65102
}
66103
if (type === 'console') {
104+
const isBlockingPageLoadError = error.message.includes(
105+
'https://nextjs.org/docs/messages/blocking-route'
106+
)
107+
if (isBlockingPageLoadError) {
108+
return 'Blocking Route'
109+
}
67110
return `Console ${error.name}`
68111
}
69112
return `Runtime ${error.name}`
@@ -237,6 +280,8 @@ Next.js version: ${props.versionInfo.installed} (${process.env.__NEXT_BUNDLER})\
237280
errorMessage={
238281
hydrationWarning ? (
239282
<HydrationErrorDescription message={hydrationWarning} />
283+
) : errorType === 'Blocking Route' ? (
284+
<BlockingPageLoadErrorDescription />
240285
) : (
241286
<GenericErrorDescription error={error} />
242287
)
@@ -349,4 +394,10 @@ export const styles = `
349394
.error-overlay-notes-container p {
350395
white-space: pre-wrap;
351396
}
397+
.nextjs__blocking_page_load_error_description {
398+
color: var(--color-stack-notes);
399+
}
400+
.nextjs__blocking_page_load_error_description_title {
401+
color: var(--color-title-color);
402+
}
352403
`

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,11 @@ export function trackAllowedDynamicAccess(
759759
)
760760
return
761761
} else {
762-
const message = `Route "${workStore.route}": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense`
762+
const message =
763+
`Route "${workStore.route}": Uncached data was accessed outside of ` +
764+
'<Suspense>. This delays the entire page from rendering, resulting in a ' +
765+
'slow user experience. Learn more: ' +
766+
'https://nextjs.org/docs/messages/blocking-route'
763767
const error = createErrorWithComponentOrOwnerStack(message, componentStack)
764768
dynamicValidation.dynamicErrors.push(error)
765769
return

test/development/app-dir/cache-components-dev-cache-scope/cache-components-dev-cache-scope.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,12 @@ describe('Cache Components Dev Errors', () => {
7575
await openRedbox(browser)
7676
desc = await getRedboxDescription(browser)
7777

78-
expect(desc).toContain(
79-
'Route "/uncached": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it'
80-
)
78+
expect(desc).toContain('https://nextjs.org/docs/messages/blocking-route')
8179

8280
await browser.refresh()
8381
await openRedbox(browser)
8482
desc = await getRedboxDescription(browser)
8583

86-
expect(desc).toContain(
87-
'Route "/uncached": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it'
88-
)
84+
expect(desc).toContain('https://nextjs.org/docs/messages/blocking-route')
8985
})
9086
})

test/development/app-dir/cache-components-dev-errors/cache-components-dev-errors.test.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,23 @@ describe('Cache Components Dev Errors', () => {
9494
})
9595

9696
expect(stripAnsi(next.cliOutput.slice(outputIndex))).toContain(
97-
`Error: Route "/no-accessed-data": ` +
98-
`A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. ` +
99-
`See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense` +
100-
'\n at Page (app/no-accessed-data/page.js:1:31)' +
101-
'\n> 1 | export default async function Page() {' +
102-
'\n | ^' +
103-
'\n 2 | await new Promise((r) => setTimeout(r, 200))' +
104-
'\n 3 | return <p>Page</p>' +
105-
'\n 4 | }'
97+
'https://nextjs.org/docs/messages/blocking-route'
10698
)
10799

108100
await expect(browser).toDisplayCollapsedRedbox(`
109101
{
110-
"description": "Route "/no-accessed-data": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
102+
"description": "Uncached data was accessed outside of <Suspense>
103+
104+
This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation.
105+
106+
To fix this, you can:
107+
108+
Wrap the component in a <Suspense> boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app.
109+
Move the asynchronous await into a Cache Component ("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user. Note that request-specific information — such as params, cookies, and headers — is not available during static prerendering.
110+
111+
Learn more: https://nextjs.org/docs/messages/blocking-route",
111112
"environmentLabel": "Server",
112-
"label": "Console Error",
113+
"label": "Blocking Route",
113114
"source": "app/no-accessed-data/page.js (1:31) @ Page
114115
> 1 | export default async function Page() {
115116
| ^",
@@ -141,17 +142,17 @@ describe('Cache Components Dev Errors', () => {
141142
const { browser, session } = sandbox
142143
if (isTurbopack) {
143144
await expect(browser).toDisplayRedbox(`
144-
{
145-
"description": "Ecmascript file had an error",
146-
"environmentLabel": null,
147-
"label": "Build Error",
148-
"source": "./app/page.tsx (1:14)
149-
Ecmascript file had an error
150-
> 1 | export const revalidate = 10
151-
| ^^^^^^^^^^",
152-
"stack": [],
153-
}
154-
`)
145+
{
146+
"description": "Ecmascript file had an error",
147+
"environmentLabel": null,
148+
"label": "Build Error",
149+
"source": "./app/page.tsx (1:14)
150+
Ecmascript file had an error
151+
> 1 | export const revalidate = 10
152+
| ^^^^^^^^^^",
153+
"stack": [],
154+
}
155+
`)
155156
} else {
156157
await expect(browser).toDisplayRedbox(`
157158
{

0 commit comments

Comments
 (0)