diff --git a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx
index 09d90f1fb27871f..3e7b3c85602d3c5 100644
--- a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx
+++ b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx
@@ -435,6 +435,9 @@ export default function HotReload({
frames: parseStack(reason.stack!),
})
}, [])
+ const handleOnReactError = useCallback(() => {
+ RuntimeErrorHandler.hadRuntimeError = true
+ }, [])
useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection)
const webSocketRef = useWebsocket(assetPrefix)
@@ -467,5 +470,9 @@ export default function HotReload({
return () => websocket && websocket.removeEventListener('message', handler)
}, [sendMessage, router, webSocketRef, dispatcher])
- return {children}
+ return (
+
+ {children}
+
+ )
}
diff --git a/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx b/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx
index 5d0c233240fb2f7..9d9eee98ea12e36 100644
--- a/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx
+++ b/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx
@@ -21,6 +21,7 @@ class ReactDevOverlay extends React.PureComponent<
{
state: OverlayState
children: React.ReactNode
+ onReactError: (error: Error) => void
},
ReactDevOverlayState
> {
@@ -40,6 +41,10 @@ class ReactDevOverlay extends React.PureComponent<
return { reactError: errorEvent }
}
+ componentDidCatch(componentErr: Error) {
+ this.props.onReactError(componentErr)
+ }
+
render() {
const { state, children } = this.props
const { reactError } = this.state
diff --git a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts
index fa86bc92f4434ca..f22ea0b9f7d8982 100644
--- a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts
+++ b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts
@@ -44,8 +44,6 @@ if (typeof window !== 'undefined') {
return
}
- RuntimeErrorHandler.hadRuntimeError = true
-
const error = ev?.error
if (
!error ||
@@ -69,8 +67,6 @@ if (typeof window !== 'undefined') {
window.addEventListener(
'unhandledrejection',
(ev: WindowEventMap['unhandledrejection']): void => {
- RuntimeErrorHandler.hadRuntimeError = true
-
const reason = ev?.reason
if (
!reason ||
diff --git a/test/development/acceptance-app/ReactRefreshLogBox.test.ts b/test/development/acceptance-app/ReactRefreshLogBox.test.ts
index 9fa41067ff307ba..b11f6b5fa285a50 100644
--- a/test/development/acceptance-app/ReactRefreshLogBox.test.ts
+++ b/test/development/acceptance-app/ReactRefreshLogBox.test.ts
@@ -114,8 +114,7 @@ describe('ReactRefreshLogBox app', () => {
await cleanup()
})
- // TODO-APP: re-enable when error recovery doesn't reload the page.
- test.skip('logbox: can recover from a event handler error', async () => {
+ test('logbox: can recover from a event handler error', async () => {
const { session, cleanup } = await sandbox(next)
await session.patch(
@@ -147,7 +146,7 @@ describe('ReactRefreshLogBox app', () => {
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
- expect(await session.hasRedbox(true)).toBe(true)
+ await session.waitForAndOpenRuntimeError()
if (process.platform === 'win32') {
expect(await session.getRedboxSource()).toMatchSnapshot()
} else {
@@ -173,6 +172,7 @@ describe('ReactRefreshLogBox app', () => {
)
expect(await session.hasRedbox()).toBe(false)
+ expect(await session.hasErrorToast()).toBe(false)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
@@ -183,6 +183,7 @@ describe('ReactRefreshLogBox app', () => {
).toBe('Count: 2')
expect(await session.hasRedbox()).toBe(false)
+ expect(await session.hasErrorToast()).toBe(false)
await cleanup()
})
diff --git a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap
index 831fe30c3b32a7d..5cf2a2113c4af11 100644
--- a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap
+++ b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap
@@ -66,6 +66,18 @@ exports[`ReactRefreshLogBox app logbox: can recover from a component error 1`] =
6 | "
`;
+exports[`ReactRefreshLogBox app logbox: can recover from a event handler error 1`] = `
+"index.js (8:18) @ eval
+
+ 6 | const increment = useCallback(() => {
+ 7 | setCount(c => c + 1)
+> 8 | throw new Error('oops')
+ | ^
+ 9 | }, [setCount])
+ 10 | return (
+ 11 | "
+`;
+
exports[`ReactRefreshLogBox app logbox: can recover from a syntax error without losing state 1`] = `
"./index.js
Error:
diff --git a/test/development/acceptance-app/helpers.ts b/test/development/acceptance-app/helpers.ts
index c97d311cb6bc74f..4fd63ce976889bb 100644
--- a/test/development/acceptance-app/helpers.ts
+++ b/test/development/acceptance-app/helpers.ts
@@ -100,6 +100,15 @@ export async function sandbox(
async hasRedbox(expected = false) {
return hasRedbox(browser, expected)
},
+ async hasErrorToast() {
+ return browser.eval(() => {
+ return Boolean(
+ Array.from(document.querySelectorAll('nextjs-portal')).find((p) =>
+ p.shadowRoot.querySelector('[data-nextjs-toast]')
+ )
+ )
+ })
+ },
async getRedboxDescription() {
return getRedboxDescription(browser)
},