Skip to content

Commit e4cc9ee

Browse files
committed
[dev-overlay] Unify error interception between app/ and pages/
1 parent 97f5dc4 commit e4cc9ee

File tree

5 files changed

+64
-125
lines changed

5 files changed

+64
-125
lines changed

Diff for: packages/next/src/client/components/errors/use-error-handler.ts

+54-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1-
import { useEffect } from 'react'
1+
import { useCallback, useEffect } from 'react'
22
import { attachHydrationErrorState } from './attach-hydration-error-state'
33
import { isNextRouterError } from '../is-next-router-error'
4-
import { storeHydrationErrorStateFromConsoleArgs } from './hydration-error-info'
4+
import {
5+
storeHydrationErrorStateFromConsoleArgs,
6+
type HydrationErrorState,
7+
} from './hydration-error-info'
58
import { formatConsoleArgs, parseConsoleArgs } from '../../lib/console'
69
import isError from '../../../lib/is-error'
710
import { createUnhandledError } from './console-error'
811
import { enqueueConsecutiveDedupedError } from './enqueue-client-error'
912
import { getReactStitchedError } from '../errors/stitched-error'
13+
import {
14+
ACTION_UNHANDLED_ERROR,
15+
ACTION_UNHANDLED_REJECTION,
16+
type useErrorOverlayReducer,
17+
} from '../react-dev-overlay/shared'
18+
import { parseStack } from '../react-dev-overlay/utils/parse-stack'
19+
import { parseComponentStack } from '../react-dev-overlay/utils/parse-component-stack'
1020

1121
const queueMicroTask =
1222
globalThis.queueMicrotask || ((cb: () => void) => Promise.resolve().then(cb))
@@ -49,7 +59,48 @@ export function handleClientError(
4959
}
5060
}
5161

52-
export function useErrorHandler(
62+
export function useErrorHandlers(
63+
dispatch: ReturnType<typeof useErrorOverlayReducer>[1]
64+
): void {
65+
const handleOnUnhandledError = useCallback(
66+
(error: Error): void => {
67+
const errorDetails = (error as any).details as
68+
| HydrationErrorState
69+
| undefined
70+
// Component stack is added to the error in use-error-handler in case there was a hydration error
71+
const componentStackTrace =
72+
(error as any)._componentStack || errorDetails?.componentStack
73+
const warning = errorDetails?.warning
74+
75+
dispatch({
76+
type: ACTION_UNHANDLED_ERROR,
77+
reason: error,
78+
frames: parseStack(error.stack || ''),
79+
componentStackFrames:
80+
typeof componentStackTrace === 'string'
81+
? parseComponentStack(componentStackTrace)
82+
: undefined,
83+
warning,
84+
})
85+
},
86+
[dispatch]
87+
)
88+
89+
const handleOnUnhandledRejection = useCallback(
90+
(reason: Error): void => {
91+
const stitchedError = getReactStitchedError(reason)
92+
dispatch({
93+
type: ACTION_UNHANDLED_REJECTION,
94+
reason: stitchedError,
95+
frames: parseStack(stitchedError.stack || ''),
96+
})
97+
},
98+
[dispatch]
99+
)
100+
useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection)
101+
}
102+
103+
function useErrorHandler(
53104
handleOnUnhandledError: ErrorHandler,
54105
handleOnUnhandledRejection: ErrorHandler
55106
) {

Diff for: packages/next/src/client/components/react-dev-overlay/app/hot-reloader-client.tsx

+3-45
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// <reference types="webpack/module.d.ts" />
22

33
import type { ReactNode } from 'react'
4-
import { useCallback, useEffect, startTransition, useMemo, useRef } from 'react'
4+
import { useEffect, startTransition, useMemo, useRef } from 'react'
55
import stripAnsi from 'next/dist/compiled/strip-ansi'
66
import formatWebpackMessages from '../utils/format-webpack-messages'
77
import { useRouter } from '../../navigation'
@@ -13,35 +13,29 @@ import {
1313
ACTION_DEV_INDICATOR,
1414
ACTION_REFRESH,
1515
ACTION_STATIC_INDICATOR,
16-
ACTION_UNHANDLED_ERROR,
17-
ACTION_UNHANDLED_REJECTION,
1816
ACTION_VERSION_INFO,
1917
REACT_REFRESH_FULL_RELOAD,
2018
reportInvalidHmrMessage,
2119
useErrorOverlayReducer,
2220
} from '../shared'
23-
import { parseStack } from '../utils/parse-stack'
2421
import { AppDevOverlay } from './app-dev-overlay'
25-
import { useErrorHandler } from '../../errors/use-error-handler'
22+
import { useErrorHandlers } from '../../errors/use-error-handler'
2623
import { RuntimeErrorHandler } from '../../errors/runtime-error-handler'
2724
import {
2825
useSendMessage,
2926
useTurbopack,
3027
useWebsocket,
3128
useWebsocketPing,
3229
} from '../utils/use-websocket'
33-
import { parseComponentStack } from '../utils/parse-component-stack'
3430
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
3531
import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../../../server/dev/hot-reloader-types'
3632
import type {
3733
HMR_ACTION_TYPES,
3834
TurbopackMsgToBrowser,
3935
} from '../../../../server/dev/hot-reloader-types'
4036
import { REACT_REFRESH_FULL_RELOAD_FROM_ERROR } from '../shared'
41-
import type { HydrationErrorState } from '../../errors/hydration-error-info'
4237
import type { DebugInfo } from '../types'
4338
import { useUntrackedPathname } from '../../navigation-untracked'
44-
import { getReactStitchedError } from '../../errors/stitched-error'
4539
import { handleDevBuildIndicatorHmrEvents } from '../../../dev/dev-build-indicator/internal/handle-dev-build-indicator-hmr-events'
4640
import type { GlobalErrorComponent } from '../../error-boundary'
4741
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
@@ -509,6 +503,7 @@ export default function HotReload({
509503
globalError: [GlobalErrorComponent, React.ReactNode]
510504
}) {
511505
const [state, dispatch] = useErrorOverlayReducer('app')
506+
useErrorHandlers(dispatch)
512507

513508
const dispatcher = useMemo<Dispatcher>(() => {
514509
return {
@@ -542,43 +537,6 @@ export default function HotReload({
542537
}
543538
}, [dispatch])
544539

545-
const handleOnUnhandledError = useCallback(
546-
(error: Error): void => {
547-
const errorDetails = (error as any).details as
548-
| HydrationErrorState
549-
| undefined
550-
// Component stack is added to the error in use-error-handler in case there was a hydration error
551-
const componentStackTrace =
552-
(error as any)._componentStack || errorDetails?.componentStack
553-
const warning = errorDetails?.warning
554-
555-
dispatch({
556-
type: ACTION_UNHANDLED_ERROR,
557-
reason: error,
558-
frames: parseStack(error.stack || ''),
559-
componentStackFrames:
560-
typeof componentStackTrace === 'string'
561-
? parseComponentStack(componentStackTrace)
562-
: undefined,
563-
warning,
564-
})
565-
},
566-
[dispatch]
567-
)
568-
569-
const handleOnUnhandledRejection = useCallback(
570-
(reason: Error): void => {
571-
const stitchedError = getReactStitchedError(reason)
572-
dispatch({
573-
type: ACTION_UNHANDLED_REJECTION,
574-
reason: stitchedError,
575-
frames: parseStack(stitchedError.stack || ''),
576-
})
577-
},
578-
[dispatch]
579-
)
580-
useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection)
581-
582540
const webSocketRef = useWebsocket(assetPrefix)
583541
useWebsocketPing(webSocketRef)
584542
const sendMessage = useSendMessage(webSocketRef)

Diff for: packages/next/src/client/components/react-dev-overlay/pages/client.ts

+4-75
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,20 @@
11
import * as Bus from './bus'
2-
import { parseStack } from '../utils/parse-stack'
3-
import { parseComponentStack } from '../utils/parse-component-stack'
4-
import {
5-
hydrationErrorState,
6-
storeHydrationErrorStateFromConsoleArgs,
7-
} from '../../errors/hydration-error-info'
82
import {
93
ACTION_BEFORE_REFRESH,
104
ACTION_BUILD_ERROR,
115
ACTION_BUILD_OK,
126
ACTION_DEV_INDICATOR,
137
ACTION_REFRESH,
148
ACTION_STATIC_INDICATOR,
15-
ACTION_UNHANDLED_ERROR,
16-
ACTION_UNHANDLED_REJECTION,
179
ACTION_VERSION_INFO,
1810
} from '../shared'
1911
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
20-
import { attachHydrationErrorState } from '../../errors/attach-hydration-error-state'
2112
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
13+
import { handleGlobalErrors } from '../../errors/use-error-handler'
14+
import { patchConsoleError } from '../../globals/intercept-console-error'
2215

2316
let isRegistered = false
2417

25-
function handleError(error: unknown) {
26-
if (!error || !(error instanceof Error) || typeof error.stack !== 'string') {
27-
// A non-error was thrown, we don't have anything to show. :-(
28-
return
29-
}
30-
31-
attachHydrationErrorState(error)
32-
33-
const componentStackTrace =
34-
(error as any)._componentStack || hydrationErrorState.componentStack
35-
const componentStackFrames =
36-
typeof componentStackTrace === 'string'
37-
? parseComponentStack(componentStackTrace)
38-
: undefined
39-
40-
// Skip ModuleBuildError and ModuleNotFoundError, as it will be sent through onBuildError callback.
41-
// This is to avoid same error as different type showing up on client to cause flashing.
42-
if (
43-
error.name !== 'ModuleBuildError' &&
44-
error.name !== 'ModuleNotFoundError'
45-
) {
46-
Bus.emit({
47-
type: ACTION_UNHANDLED_ERROR,
48-
reason: error,
49-
frames: parseStack(error.stack),
50-
componentStackFrames,
51-
})
52-
}
53-
}
54-
55-
let origConsoleError = console.error
56-
function nextJsHandleConsoleError(...args: any[]) {
57-
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
58-
const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0]
59-
storeHydrationErrorStateFromConsoleArgs(...args)
60-
handleError(error)
61-
origConsoleError.apply(window.console, args)
62-
}
63-
64-
function onUnhandledError(event: ErrorEvent) {
65-
const error = event?.error
66-
handleError(error)
67-
}
68-
69-
function onUnhandledRejection(ev: PromiseRejectionEvent) {
70-
const reason = ev?.reason
71-
if (
72-
!reason ||
73-
!(reason instanceof Error) ||
74-
typeof reason.stack !== 'string'
75-
) {
76-
// A non-error was thrown, we don't have anything to show. :-(
77-
return
78-
}
79-
80-
const e = reason
81-
Bus.emit({
82-
type: ACTION_UNHANDLED_REJECTION,
83-
reason: reason,
84-
frames: parseStack(e.stack!),
85-
})
86-
}
87-
8818
export function register() {
8919
if (isRegistered) {
9020
return
@@ -95,9 +25,8 @@ export function register() {
9525
Error.stackTraceLimit = 50
9626
} catch {}
9727

98-
window.addEventListener('error', onUnhandledError)
99-
window.addEventListener('unhandledrejection', onUnhandledRejection)
100-
window.console.error = nextJsHandleConsoleError
28+
handleGlobalErrors()
29+
patchConsoleError()
10130
}
10231

10332
export function onBuildOk() {

Diff for: packages/next/src/client/components/react-dev-overlay/pages/hooks.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from 'react'
22
import * as Bus from './bus'
3-
import { useErrorOverlayReducer } from '../shared'
3+
import { useErrorHandlers, useErrorOverlayReducer } from '../shared'
44
import { Router } from '../../../router'
55

66
export const usePagesDevOverlay = () => {
77
const [state, dispatch] = useErrorOverlayReducer('pages')
8+
useErrorHandlers(dispatch)
89

910
React.useEffect(() => {
1011
Bus.on(dispatch)

Diff for: packages/next/src/client/components/react-dev-overlay/shared.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function pushErrorFilterDuplicates(
117117
const shouldDisableDevIndicator =
118118
process.env.__NEXT_DEV_INDICATOR?.toString() === 'false'
119119

120-
export const INITIAL_OVERLAY_STATE: Omit<OverlayState, 'routerType'> = {
120+
const INITIAL_OVERLAY_STATE: Omit<OverlayState, 'routerType'> = {
121121
nextId: 1,
122122
buildError: null,
123123
errors: [],

0 commit comments

Comments
 (0)