Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import { useState, useRef, useLayoutEffect } from 'react'
import type { ErrorType } from '../error-type-label/error-type-label'

export type ErrorMessageType = React.ReactNode

type ErrorMessageProps = {
errorMessage: ErrorMessageType
errorType: ErrorType
}

export function ErrorMessage({ errorMessage }: ErrorMessageProps) {
export function ErrorMessage({ errorMessage, errorType }: ErrorMessageProps) {
const [isExpanded, setIsExpanded] = useState(false)
const [shouldTruncate, setShouldTruncate] = useState(false)
const messageRef = useRef<HTMLParagraphElement>(null)
const [isTooTall, setIsTooTall] = useState(false)
const messageRef = useRef<HTMLDivElement>(null)

useLayoutEffect(() => {
if (messageRef.current) {
setShouldTruncate(messageRef.current.scrollHeight > 200)
setIsTooTall(messageRef.current.scrollHeight > 200)
}
}, [errorMessage])

// The "Blocking Route" error message is specifically formatted to look nice
// in the overlay (rather than just passed through from the console), so we
// intentionally don't truncate it and rely on the scroll overflow instead.
const shouldTruncate = isTooTall && errorType !== 'Blocking Route'

return (
<div className="nextjs__container_errors_wrapper">
<p
<div
ref={messageRef}
id="nextjs__container_errors_desc"
className={`nextjs__container_errors_desc ${shouldTruncate && !isExpanded ? 'truncated' : ''}`}
>
{errorMessage}
</p>
</div>
{shouldTruncate && !isExpanded && (
<>
<div className="nextjs__container_errors_gradient_overlay" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ export function ErrorOverlayLayout({
generateErrorInfo={generateErrorInfo}
/>
</div>
<ErrorMessage errorMessage={errorMessage} />
<ErrorMessage
errorMessage={errorMessage}
errorType={errorType}
/>
</ErrorOverlayDialogHeader>

<ErrorOverlayDialogBody>{children}</ErrorOverlayDialogBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type ErrorType =
| `Runtime ${string}`
| `Console ${string}`
| `Recoverable ${string}`
| 'Blocking Route'

type ErrorTypeLabelProps = {
errorType: ErrorType
Expand All @@ -12,7 +13,7 @@ export function ErrorTypeLabel({ errorType }: ErrorTypeLabelProps) {
return (
<span
id="nextjs__container_errors_label"
className="nextjs__container_errors_label"
className={`nextjs__container_errors_label ${errorType === 'Blocking Route' ? 'nextjs__container_errors_label_blocking_page' : ''}`}
>
{errorType}
</span>
Expand All @@ -31,4 +32,9 @@ export const styles = `
font-family: var(--font-stack-monospace);
line-height: var(--size-20);
}

.nextjs__container_errors_label_blocking_page {
background: var(--color-blue-100);
color: var(--color-blue-900);
}
`
51 changes: 51 additions & 0 deletions packages/next/src/next-devtools/dev-overlay/container/errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,43 @@ function GenericErrorDescription({ error }: { error: Error }) {
)
}

function BlockingPageLoadErrorDescription() {
return (
<div className="nextjs__blocking_page_load_error_description">
<h3 className="nextjs__blocking_page_load_error_description_title">
Uncached data was accessed outside of {'<Suspense>'}
</h3>
<p>
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.
</p>
<p>To fix this, you can:</p>
<ul>
<li>
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.
</li>
<li>
Move the asynchronous await into a Cache Component (
<code>"use cache"</code>). 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 &mdash; such as
params, cookies, and headers &mdash; is not available during static
prerendering.
</li>
</ul>
<p>
Learn more:{' '}
<a href="https://nextjs.org/docs/messages/blocking-route">
https://nextjs.org/docs/messages/blocking-route
</a>
</p>
</div>
)
}

export function getErrorTypeLabel(
error: Error,
type: ReadyRuntimeError['type']
Expand All @@ -64,6 +101,12 @@ export function getErrorTypeLabel(
return `Recoverable ${error.name}`
}
if (type === 'console') {
const isBlockingPageLoadError = error.message.includes(
'https://nextjs.org/docs/messages/blocking-route'
)
if (isBlockingPageLoadError) {
return 'Blocking Route'
}
return `Console ${error.name}`
}
return `Runtime ${error.name}`
Expand Down Expand Up @@ -237,6 +280,8 @@ Next.js version: ${props.versionInfo.installed} (${process.env.__NEXT_BUNDLER})\
errorMessage={
hydrationWarning ? (
<HydrationErrorDescription message={hydrationWarning} />
) : errorType === 'Blocking Route' ? (
<BlockingPageLoadErrorDescription />
) : (
<GenericErrorDescription error={error} />
)
Expand Down Expand Up @@ -349,4 +394,10 @@ export const styles = `
.error-overlay-notes-container p {
white-space: pre-wrap;
}
.nextjs__blocking_page_load_error_description {
color: var(--color-stack-notes);
}
.nextjs__blocking_page_load_error_description_title {
color: var(--color-title-color);
}
`
6 changes: 5 additions & 1 deletion packages/next/src/server/app-render/dynamic-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,11 @@ export function trackAllowedDynamicAccess(
)
return
} else {
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`
const message =
`Route "${workStore.route}": Uncached data was accessed outside of ` +
'<Suspense>. This delays the entire page from rendering, resulting in a ' +
'slow user experience. Learn more: ' +
'https://nextjs.org/docs/messages/blocking-route'
const error = createErrorWithComponentOrOwnerStack(message, componentStack)
dynamicValidation.dynamicErrors.push(error)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,12 @@ describe('Cache Components Dev Errors', () => {
await openRedbox(browser)
desc = await getRedboxDescription(browser)

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

await browser.refresh()
await openRedbox(browser)
desc = await getRedboxDescription(browser)

expect(desc).toContain(
'Route "/uncached": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it'
)
expect(desc).toContain('https://nextjs.org/docs/messages/blocking-route')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -94,22 +94,23 @@ describe('Cache Components Dev Errors', () => {
})

expect(stripAnsi(next.cliOutput.slice(outputIndex))).toContain(
`Error: 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` +
'\n at Page (app/no-accessed-data/page.js:1:31)' +
'\n> 1 | export default async function Page() {' +
'\n | ^' +
'\n 2 | await new Promise((r) => setTimeout(r, 200))' +
'\n 3 | return <p>Page</p>' +
'\n 4 | }'
'https://nextjs.org/docs/messages/blocking-route'
)

await expect(browser).toDisplayCollapsedRedbox(`
{
"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",
"description": "Uncached data was accessed outside of <Suspense>

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.

To fix this, you can:

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.
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.

Learn more: https://nextjs.org/docs/messages/blocking-route",
"environmentLabel": "Server",
"label": "Console Error",
"label": "Blocking Route",
"source": "app/no-accessed-data/page.js (1:31) @ Page
> 1 | export default async function Page() {
| ^",
Expand Down Expand Up @@ -141,17 +142,17 @@ describe('Cache Components Dev Errors', () => {
const { browser, session } = sandbox
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Ecmascript file had an error",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.tsx (1:14)
Ecmascript file had an error
> 1 | export const revalidate = 10
| ^^^^^^^^^^",
"stack": [],
}
`)
{
"description": "Ecmascript file had an error",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.tsx (1:14)
Ecmascript file had an error
> 1 | export const revalidate = 10
| ^^^^^^^^^^",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
Expand Down
Loading
Loading