Skip to content

Commit

Permalink
Fix race condition in useErrorHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
alexkirsz committed Nov 7, 2022
1 parent f57adc7 commit 1434110
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 91 deletions.
Expand Up @@ -21,7 +21,10 @@ import {
} from './internal/error-overlay-reducer'
import { parseStack } from './internal/helpers/parseStack'
import ReactDevOverlay from './internal/ReactDevOverlay'
import { useErrorHandler } from './internal/helpers/use-error-handler'
import {
RuntimeErrorHandler,
useErrorHandler,
} from './internal/helpers/use-error-handler'
import {
useSendMessage,
useWebsocket,
Expand All @@ -40,7 +43,6 @@ type PongEvent = any

let mostRecentCompilationHash: any = null
let __nextDevClientId = Math.round(Math.random() * 100 + Date.now())
let hadRuntimeError = false

// let startLatency = undefined

Expand Down Expand Up @@ -123,7 +125,7 @@ function tryApplyUpdates(
}

function handleApplyUpdates(err: any, updatedModules: any) {
if (err || hadRuntimeError || !updatedModules) {
if (err || RuntimeErrorHandler.hadRuntimeError || !updatedModules) {
if (err) {
console.warn(
'[Fast Refresh] performing full reload\n\n' +
Expand All @@ -133,7 +135,7 @@ function tryApplyUpdates(
'It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.\n' +
'Fast Refresh requires at least one parent function component in your React tree.'
)
} else if (hadRuntimeError) {
} else if (RuntimeErrorHandler.hadRuntimeError) {
console.warn(
'[Fast Refresh] performing full reload because your application had an unrecoverable error'
)
Expand Down Expand Up @@ -333,7 +335,7 @@ function processMessage(
clientId: __nextDevClientId,
})
)
if (hadRuntimeError) {
if (RuntimeErrorHandler.hadRuntimeError) {
return window.location.reload()
}
startTransition(() => {
Expand Down Expand Up @@ -407,66 +409,20 @@ export default function HotReload({
}
}, [dispatch])

const handleOnUnhandledError = useCallback(
(ev: WindowEventMap['error']): void => {
if (
ev.error &&
ev.error.digest &&
(ev.error.digest.startsWith('NEXT_REDIRECT') ||
ev.error.digest === 'NEXT_NOT_FOUND')
) {
ev.preventDefault()
return
}

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

if (
error.message.match(/(hydration|content does not match|did not match)/i)
) {
error.message += `\n\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error`
}

const e = error
dispatch({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(e.stack!),
})
},
[]
)
const handleOnUnhandledRejection = useCallback(
(ev: WindowEventMap['unhandledrejection']): void => {
hadRuntimeError = true
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
dispatch({
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(e.stack!),
})
},
[]
)
const handleOnUnhandledError = useCallback((error: Error): void => {
dispatch({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(error.stack!),
})
}, [])
const handleOnUnhandledRejection = useCallback((reason: Error): void => {
dispatch({
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(reason.stack!),
})
}, [])
useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection)

const webSocketRef = useWebsocket(assetPrefix)
Expand Down
@@ -1,34 +1,105 @@
import { useEffect, useRef } from 'react'
import { useEffect } from 'react'

export type ErrorHandler = (error: Error) => void

export const RuntimeErrorHandler = {
hadRuntimeError: false,
}

function isNextRouterError(error: any): boolean {
return (
error &&
error.digest &&
(error.digest.startsWith('NEXT_REDIRECT') ||
error.digest === 'NEXT_NOT_FOUND')
)
}

function isHydrationError(error: Error): boolean {
return (
error.message.match(/(hydration|content does not match|did not match)/i) !=
null
)
}

try {
Error.stackTraceLimit = 50
} catch {}

const errorQueue: Array<Error> = []
const rejectionQueue: Array<Error> = []
const errorHandlers: Array<ErrorHandler> = []
const rejectionHandlers: Array<ErrorHandler> = []

// These event handlers must be added outside of the hook because there is no
// guarantee that the hook will be alive in a mounted component in time to
// when the errors occur.
window.addEventListener('error', (ev: WindowEventMap['error']): void => {
if (isNextRouterError(ev.error)) {
ev.preventDefault()
return
}

RuntimeErrorHandler.hadRuntimeError = true

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

if (isHydrationError(error)) {
error.message += `\n\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error`
}

const e = error
errorQueue.push(e)
for (const handler of errorHandlers) {
handler(e)
}
})
window.addEventListener(
'unhandledrejection',
(ev: WindowEventMap['unhandledrejection']): void => {
RuntimeErrorHandler.hadRuntimeError = true

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
rejectionQueue.push(e)
for (const handler of rejectionHandlers) {
handler(e)
}
}
)

export function useErrorHandler(
handleOnUnhandledError: (event: WindowEventMap['error']) => void,
handleOnUnhandledRejection: (
event: WindowEventMap['unhandledrejection']
) => void
handleOnUnhandledError: ErrorHandler,
handleOnUnhandledRejection: ErrorHandler
) {
const stacktraceLimitRef = useRef<undefined | number>()

useEffect(() => {
try {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 50
stacktraceLimitRef.current = limit
} catch {}

window.addEventListener('error', handleOnUnhandledError)
window.addEventListener('unhandledrejection', handleOnUnhandledRejection)
// Handle queued errors.
errorQueue.forEach(handleOnUnhandledError)
rejectionQueue.forEach(handleOnUnhandledRejection)

// Listen to new errors.
errorHandlers.push(handleOnUnhandledError)
rejectionHandlers.push(handleOnUnhandledRejection)

return () => {
if (stacktraceLimitRef.current !== undefined) {
try {
Error.stackTraceLimit = stacktraceLimitRef.current
} catch {}
stacktraceLimitRef.current = undefined
}

window.removeEventListener('error', handleOnUnhandledError)
window.removeEventListener(
'unhandledrejection',
handleOnUnhandledRejection
// Remove listeners.
errorHandlers.splice(errorHandlers.indexOf(handleOnUnhandledError), 1)
rejectionHandlers.splice(
rejectionHandlers.indexOf(handleOnUnhandledRejection),
1
)
}
}, [handleOnUnhandledError, handleOnUnhandledRejection])
Expand Down

0 comments on commit 1434110

Please sign in to comment.