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
11 changes: 6 additions & 5 deletions docs/01-app/04-api-reference/03-file-conventions/error.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,12 @@ export default function GlobalError({ error, reset }) {
}
```

> **Good to know**: `global-error.js` is only enabled in production. In development, our error overlay will show instead.
> **Good to know**: `global-error.js` is always displayed In development, error overlay will show instead.

## Version History

| Version | Changes |
| --------- | -------------------------- |
| `v13.1.0` | `global-error` introduced. |
| `v13.0.0` | `error` introduced. |
| Version | Changes |
| --------- | ------------------------------------------- |
| `v15.2.0` | display `global-error` also in development. |
| `v13.1.0` | `global-error` introduced. |
| `v13.0.0` | `error` introduced. |
25 changes: 20 additions & 5 deletions packages/next/src/client/components/app-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ import {
PathParamsContext,
} from '../../shared/lib/hooks-client-context.shared-runtime'
import { useReducer, useUnwrapState } from './use-reducer'
import { ErrorBoundary, type ErrorComponent } from './error-boundary'
import {
ErrorBoundary,
type ErrorComponent,
type GlobalErrorComponent,
} from './error-boundary'
import { isBot } from '../../shared/lib/router/utils/is-bot'
import { addBasePath } from '../add-base-path'
import { AppRouterAnnouncer } from './app-router-announcer'
Expand Down Expand Up @@ -242,9 +246,11 @@ function Head({
function Router({
actionQueue,
assetPrefix,
globalError,
}: {
actionQueue: AppRouterActionQueue
assetPrefix: string
globalError: [GlobalErrorComponent, React.ReactNode]
}) {
const [state, dispatch] = useReducer(actionQueue)
const { canonicalUrl } = useUnwrapState(state)
Expand Down Expand Up @@ -622,7 +628,11 @@ function Router({
const HotReloader: typeof import('./react-dev-overlay/app/hot-reloader-client').default =
require('./react-dev-overlay/app/hot-reloader-client').default

content = <HotReloader assetPrefix={assetPrefix}>{content}</HotReloader>
content = (
<HotReloader assetPrefix={assetPrefix} globalError={globalError}>
{content}
</HotReloader>
)
}

return (
Expand Down Expand Up @@ -654,17 +664,22 @@ export default function AppRouter({
assetPrefix,
}: {
actionQueue: AppRouterActionQueue
globalErrorComponentAndStyles: [ErrorComponent, React.ReactNode | undefined]
globalErrorComponentAndStyles: [GlobalErrorComponent, React.ReactNode]
assetPrefix: string
}) {
useNavFailureHandler()

return (
<ErrorBoundary
errorComponent={globalErrorComponent}
// globalErrorComponent doesn't need `reset`, we do a type cast here to fit the ErrorBoundary type
errorComponent={globalErrorComponent as ErrorComponent}
errorStyles={globalErrorStyles}
>
<Router actionQueue={actionQueue} assetPrefix={assetPrefix} />
<Router
actionQueue={actionQueue}
assetPrefix={assetPrefix}
globalError={[globalErrorComponent, globalErrorStyles]}
/>
</ErrorBoundary>
)
}
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/client/components/error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ export class ErrorBoundaryHandler extends React.Component<
}
}

export type GlobalErrorComponent = React.ComponentType<{
error: any
}>
export function GlobalError({ error }: { error: any }) {
const digest: string | undefined = error?.digest
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,48 @@ import { CssReset } from '../internal/styles/css-reset'
import { RootLayoutMissingTagsError } from '../internal/container/root-layout-missing-tags-error'
import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler'
import { Colors } from '../internal/styles/colors'
import type { GlobalErrorComponent } from '../../../error-boundary'

function ErroredHtml({
globalError: [GlobalError, globalErrorStyles],
error,
}: {
globalError: [GlobalErrorComponent, React.ReactNode]
error: unknown
}) {
if (!error) {
return (
<html>
<head />
<body />
</html>
)
}
return (
<>
{globalErrorStyles}
<GlobalError error={error} />
</>
)
}

interface ReactDevOverlayState {
reactError?: unknown
isReactError: boolean
}
export default class ReactDevOverlay extends React.PureComponent<
{
state: OverlayState
dispatcher?: Dispatcher
globalError: [GlobalErrorComponent, React.ReactNode]
children: React.ReactNode
},
ReactDevOverlayState
> {
state = { isReactError: false }
state = {
reactError: null,
isReactError: false,
}

static getDerivedStateFromError(error: Error): ReactDevOverlayState {
if (!error.stack) return { isReactError: false }
Expand All @@ -36,8 +65,8 @@ export default class ReactDevOverlay extends React.PureComponent<
}

render() {
const { state, children } = this.props
const { isReactError } = this.state
const { state, children, globalError } = this.props
const { isReactError, reactError } = this.state

const hasBuildError = state.buildError != null
const hasStaticIndicator = state.staticIndicator
Expand All @@ -48,10 +77,7 @@ export default class ReactDevOverlay extends React.PureComponent<
return (
<>
{isReactError ? (
<html>
<head></head>
<body></body>
</html>
<ErroredHtml globalError={globalError} error={reactError} />
) : (
children
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDevOverlay from './react-dev-overlay'
import { getSocketUrl } from '../internal/helpers/get-socket-url'
import { INITIAL_OVERLAY_STATE } from '../shared'
import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../../../server/dev/hot-reloader-types'
import GlobalError from '../../error-boundary'

// if an error is thrown while rendering an RSC stream, this will catch it in dev
// and show the error overlay
Expand Down Expand Up @@ -42,6 +43,7 @@ export function createRootLevelDevOverlayElement(reactEl: React.ReactElement) {
<FallbackLayout>
<ReactDevOverlay
state={{ ...INITIAL_OVERLAY_STATE, rootLayoutMissingTags }}
globalError={[GlobalError, null]}
>
{reactEl}
</ReactDevOverlay>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { useUntrackedPathname } from '../../navigation-untracked'
import { getReactStitchedError } from '../../errors/stitched-error'
import { shouldRenderRootLevelErrorOverlay } from '../../../lib/is-error-thrown-while-rendering-rsc'
import { handleDevBuildIndicatorHmrEvents } from '../../../dev/dev-build-indicator/internal/handle-dev-build-indicator-hmr-events'
import type { GlobalErrorComponent } from '../../error-boundary'

export interface Dispatcher {
onBuildOk(): void
Expand Down Expand Up @@ -538,9 +539,11 @@ function processMessage(
export default function HotReload({
assetPrefix,
children,
globalError,
}: {
assetPrefix: string
children?: ReactNode
children: ReactNode
globalError: [GlobalErrorComponent, React.ReactNode]
}) {
const [state, dispatch] = useErrorOverlayReducer()

Expand Down Expand Up @@ -724,7 +727,11 @@ export default function HotReload({

if (shouldRenderErrorOverlay) {
return (
<ReactDevOverlay state={state} dispatcher={dispatcher}>
<ReactDevOverlay
state={state}
dispatcher={dispatcher}
globalError={globalError}
>
{children}
</ReactDevOverlay>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,62 @@ import { CssReset } from '../internal/styles/CssReset'
import { RootLayoutMissingTagsError } from '../internal/container/RootLayoutMissingTagsError'
import type { Dispatcher } from './hot-reloader-client'
import { RuntimeErrorHandler } from '../../errors/runtime-error-handler'
import type { GlobalErrorComponent } from '../../error-boundary'

function ErroredHtml({
globalError: [GlobalError, globalErrorStyles],
error,
}: {
globalError: [GlobalErrorComponent, React.ReactNode]
error: unknown
}) {
if (!error) {
return (
<html>
<head />
<body />
</html>
)
}
return (
<>
{globalErrorStyles}
<GlobalError error={error} />
</>
)
}

interface ReactDevOverlayState {
reactError?: unknown
isReactError: boolean
}
export default class ReactDevOverlay extends React.PureComponent<
{
state: OverlayState
globalError: [GlobalErrorComponent, React.ReactNode]
dispatcher?: Dispatcher
children: React.ReactNode
},
ReactDevOverlayState
> {
state = { isReactError: false }
state = {
reactError: null,
isReactError: false,
}

static getDerivedStateFromError(error: Error): ReactDevOverlayState {
if (!error.stack) return { isReactError: false }

RuntimeErrorHandler.hadRuntimeError = true
return {
reactError: error,
isReactError: true,
}
}

render() {
const { state, children, dispatcher } = this.props
const { isReactError } = this.state
const { state, children, dispatcher, globalError } = this.props
const { isReactError, reactError } = this.state

const hasBuildError = state.buildError != null
const hasRuntimeErrors = Boolean(state.errors.length)
Expand All @@ -45,10 +75,7 @@ export default class ReactDevOverlay extends React.PureComponent<
return (
<>
{isReactError ? (
<html>
<head></head>
<body></body>
</html>
<ErroredHtml globalError={globalError} error={reactError} />
) : (
children
)}
Expand Down
Loading