Skip to content

[dev-overlay] Unify error interception between app/ and pages/ #77326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: canary
Choose a base branch
from
Draft
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
46 changes: 44 additions & 2 deletions packages/next/src/client/components/errors/use-error-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react'
import { useCallback, useEffect } from 'react'
import { attachHydrationErrorState } from './attach-hydration-error-state'
import { isNextRouterError } from '../is-next-router-error'
import { storeHydrationErrorStateFromConsoleArgs } from './hydration-error-info'
Expand All @@ -7,6 +7,13 @@ import isError from '../../../lib/is-error'
import { createConsoleError } from './console-error'
import { enqueueConsecutiveDedupedError } from './enqueue-client-error'
import { getReactStitchedError } from '../errors/stitched-error'
import {
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
type useErrorOverlayReducer,
} from '../react-dev-overlay/shared'
import { parseStack } from '../react-dev-overlay/utils/parse-stack'
import { parseComponentStack } from '../react-dev-overlay/utils/parse-component-stack'

const queueMicroTask =
globalThis.queueMicrotask || ((cb: () => void) => Promise.resolve().then(cb))
Expand Down Expand Up @@ -70,7 +77,42 @@ export function handleClientError(originError: unknown) {
}
}

export function useErrorHandler(
export function useErrorHandlers(
dispatch: ReturnType<typeof useErrorOverlayReducer>[1]
): void {
const handleOnUnhandledError = useCallback(
(error: Error): void => {
// Component stack is added to the error in use-error-handler in case there was a hydration error
const componentStackTrace = (error as any)._componentStack

dispatch({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(error.stack || ''),
componentStackFrames:
typeof componentStackTrace === 'string'
? parseComponentStack(componentStackTrace)
: undefined,
})
},
[dispatch]
)

const handleOnUnhandledRejection = useCallback(
(reason: Error): void => {
const stitchedError = getReactStitchedError(reason)
dispatch({
type: ACTION_UNHANDLED_REJECTION,
reason: stitchedError,
frames: parseStack(stitchedError.stack || ''),
})
},
[dispatch]
)
useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection)
}

function useErrorHandler(
handleOnUnhandledError: ErrorHandler,
handleOnUnhandledRejection: ErrorHandler
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="webpack/module.d.ts" />

import type { ReactNode } from 'react'
import { useCallback, useEffect, startTransition, useMemo, useRef } from 'react'
import { useEffect, startTransition, useMemo, useRef } from 'react'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import formatWebpackMessages from '../utils/format-webpack-messages'
import { useRouter } from '../../navigation'
Expand All @@ -13,24 +13,20 @@ import {
ACTION_DEV_INDICATOR,
ACTION_REFRESH,
ACTION_STATIC_INDICATOR,
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
ACTION_VERSION_INFO,
REACT_REFRESH_FULL_RELOAD,
reportInvalidHmrMessage,
useErrorOverlayReducer,
} from '../shared'
import { parseStack } from '../utils/parse-stack'
import { AppDevOverlay } from './app-dev-overlay'
import { useErrorHandler } from '../../errors/use-error-handler'
import { useErrorHandlers } from '../../errors/use-error-handler'
import { RuntimeErrorHandler } from '../../errors/runtime-error-handler'
import {
useSendMessage,
useTurbopack,
useWebsocket,
useWebsocketPing,
} from '../utils/use-websocket'
import { parseComponentStack } from '../utils/parse-component-stack'
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../../../server/dev/hot-reloader-types'
import type {
Expand All @@ -40,7 +36,6 @@ import type {
import { REACT_REFRESH_FULL_RELOAD_FROM_ERROR } from '../shared'
import type { DebugInfo } from '../types'
import { useUntrackedPathname } from '../../navigation-untracked'
import { getReactStitchedError } from '../../errors/stitched-error'
import { handleDevBuildIndicatorHmrEvents } from '../../../dev/dev-build-indicator/internal/handle-dev-build-indicator-hmr-events'
import type { GlobalErrorComponent } from '../../error-boundary'
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
Expand Down Expand Up @@ -480,6 +475,7 @@ export default function HotReload({
globalError: [GlobalErrorComponent, React.ReactNode]
}) {
const [state, dispatch] = useErrorOverlayReducer('app')
useErrorHandlers(dispatch)

const dispatcher = useMemo<Dispatcher>(() => {
return {
Expand Down Expand Up @@ -513,37 +509,6 @@ export default function HotReload({
}
}, [dispatch])

const handleOnUnhandledError = useCallback(
(error: Error): void => {
// Component stack is added to the error in use-error-handler in case there was a hydration error
const componentStackTrace = (error as any)._componentStack

dispatch({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(error.stack || ''),
componentStackFrames:
typeof componentStackTrace === 'string'
? parseComponentStack(componentStackTrace)
: undefined,
})
},
[dispatch]
)

const handleOnUnhandledRejection = useCallback(
(reason: Error): void => {
const stitchedError = getReactStitchedError(reason)
dispatch({
type: ACTION_UNHANDLED_REJECTION,
reason: stitchedError,
frames: parseStack(stitchedError.stack || ''),
})
},
[dispatch]
)
useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection)

const webSocketRef = useWebsocket(assetPrefix)
useWebsocketPing(webSocketRef)
const sendMessage = useSendMessage(webSocketRef)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,86 +1,20 @@
import * as Bus from './bus'
import { parseStack } from '../utils/parse-stack'
import { parseComponentStack } from '../utils/parse-component-stack'
import { storeHydrationErrorStateFromConsoleArgs } from '../../errors/hydration-error-info'
import {
ACTION_BEFORE_REFRESH,
ACTION_BUILD_ERROR,
ACTION_BUILD_OK,
ACTION_DEV_INDICATOR,
ACTION_REFRESH,
ACTION_STATIC_INDICATOR,
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
ACTION_VERSION_INFO,
} from '../shared'
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
import { attachHydrationErrorState } from '../../errors/attach-hydration-error-state'
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
import { handleGlobalErrors } from '../../errors/use-error-handler'
import { patchConsoleError } from '../../globals/intercept-console-error'

let isRegistered = false

function handleError(error: unknown) {
if (!error || !(error instanceof Error) || typeof error.stack !== 'string') {
// A non-error was thrown, we don't have anything to show. :-(
return
}

attachHydrationErrorState(error)

const componentStackTrace = (error as any)._componentStack
const componentStackFrames =
typeof componentStackTrace === 'string'
? parseComponentStack(componentStackTrace)
: undefined

// Skip ModuleBuildError and ModuleNotFoundError, as it will be sent through onBuildError callback.
// This is to avoid same error as different type showing up on client to cause flashing.
if (
error.name !== 'ModuleBuildError' &&
error.name !== 'ModuleNotFoundError'
) {
Bus.emit({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(error.stack),
componentStackFrames,
})
}
Comment on lines -36 to -48
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is effectively removed but it's also not working at the moment as far as I can tell. Tracking in https://linear.app/vercel/issue/NDX-990

}

let origConsoleError = console.error
function nextJsHandleConsoleError(...args: any[]) {
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0]
storeHydrationErrorStateFromConsoleArgs(...args)
handleError(error)
origConsoleError.apply(window.console, args)
}

function onUnhandledError(event: ErrorEvent) {
const error = event?.error
handleError(error)
}

function onUnhandledRejection(ev: PromiseRejectionEvent) {
const reason = ev?.reason
if (
!reason ||
!(reason instanceof Error) ||
typeof reason.stack !== 'string'
) {
// A non-error was thrown, we don't have anything to show. :-(
return
}

const e = reason
Bus.emit({
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(e.stack!),
})
}

export function register() {
if (isRegistered) {
return
Expand All @@ -91,9 +25,8 @@ export function register() {
Error.stackTraceLimit = 50
} catch {}

window.addEventListener('error', onUnhandledError)
window.addEventListener('unhandledrejection', onUnhandledRejection)
window.console.error = nextJsHandleConsoleError
handleGlobalErrors()
patchConsoleError()
}

export function onBuildOk() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react'
import * as Bus from './bus'
import { useErrorOverlayReducer } from '../shared'
import { useErrorHandlers } from '../../errors/use-error-handler'
import { Router } from '../../../router'

export const usePagesDevOverlay = () => {
const [state, dispatch] = useErrorOverlayReducer('pages')
useErrorHandlers(dispatch)

React.useEffect(() => {
Bus.on(dispatch)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function pushErrorFilterDuplicates(
const shouldDisableDevIndicator =
process.env.__NEXT_DEV_INDICATOR?.toString() === 'false'

export const INITIAL_OVERLAY_STATE: Omit<OverlayState, 'routerType'> = {
const INITIAL_OVERLAY_STATE: Omit<OverlayState, 'routerType'> = {
nextId: 1,
buildError: null,
errors: [],
Expand Down
Loading