diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 5ed91e18775fe..cc2de85c03f14 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -462,6 +462,14 @@ export function renderError(renderErrorProps: RenderErrorProps): Promise { return pageLoader .loadPage('/_error') .then(({ page: ErrorComponent, styleSheets }) => { + return lastAppProps?.Component === ErrorComponent + ? import('../pages/_error').then((m) => ({ + ErrorComponent: m.default as React.ComponentType<{}>, + styleSheets: [], + })) + : { ErrorComponent, styleSheets } + }) + .then(({ ErrorComponent, styleSheets }) => { // In production we do a normal render with the `ErrorComponent` as component. // If we've gotten here upon initial render, we can use the props from the server. // Otherwise, we need to call `getInitialProps` on `App` before mounting. diff --git a/packages/next/taskfile-babel.js b/packages/next/taskfile-babel.js index 90e2d9046f17e..b699f15346dd8 100644 --- a/packages/next/taskfile-babel.js +++ b/packages/next/taskfile-babel.js @@ -6,26 +6,26 @@ const path = require('path') // eslint-disable-next-line import/no-extraneous-dependencies const transform = require('@babel/core').transform +const babelClientPresetEnvOptions = { + modules: 'commonjs', + targets: { + esmodules: true, + }, + bugfixes: true, + loose: true, + // This is handled by the Next.js webpack config that will run next/babel over the same code. + exclude: [ + 'transform-typeof-symbol', + 'transform-async-to-generator', + 'transform-spread', + 'proposal-dynamic-import', + ], +} + const babelClientOpts = { presets: [ '@babel/preset-typescript', - [ - '@babel/preset-env', - { - modules: 'commonjs', - targets: { - esmodules: true, - }, - bugfixes: true, - loose: true, - // This is handled by the Next.js webpack config that will run next/babel over the same code. - exclude: [ - 'transform-typeof-symbol', - 'transform-async-to-generator', - 'transform-spread', - ], - }, - ], + ['@babel/preset-env', babelClientPresetEnvOptions], ['@babel/preset-react', { useBuiltIns: true }], ], plugins: [ diff --git a/test/integration/custom-error-page-exception/pages/_error.js b/test/integration/custom-error-page-exception/pages/_error.js new file mode 100644 index 0000000000000..1e5fccdc31d67 --- /dev/null +++ b/test/integration/custom-error-page-exception/pages/_error.js @@ -0,0 +1,12 @@ +/* eslint-disable no-unused-expressions, no-undef */ +let renderCount = 0 + +export default function Error() { + renderCount++ + + // Guard to avoid endless loop crashing the browser tab. + if (typeof window !== 'undefined' && renderCount < 3) { + throw new Error('crash') + } + return `error threw ${renderCount} times` +} diff --git a/test/integration/custom-error-page-exception/pages/index.js b/test/integration/custom-error-page-exception/pages/index.js new file mode 100644 index 0000000000000..1314e09108650 --- /dev/null +++ b/test/integration/custom-error-page-exception/pages/index.js @@ -0,0 +1,20 @@ +/* eslint-disable no-unused-expressions, no-unused-vars */ +import React from 'react' +import Link from 'next/link' + +function page() { + return ( + + Client side nav + + ) +} + +page.getInitialProps = () => { + if (typeof window !== 'undefined') { + throw new Error('Oops from Home') + } + return {} +} + +export default page diff --git a/test/integration/custom-error-page-exception/test/index.test.js b/test/integration/custom-error-page-exception/test/index.test.js new file mode 100644 index 0000000000000..c95c35ec35cda --- /dev/null +++ b/test/integration/custom-error-page-exception/test/index.test.js @@ -0,0 +1,26 @@ +/* eslint-env jest */ + +import { join } from 'path' +import webdriver from 'next-webdriver' +import { nextBuild, nextStart, findPort, killApp } from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 1) + +const appDir = join(__dirname, '..') +const navSel = '#nav' +const errorMessage = 'Application error: a client-side exception has occurred' + +describe('Custom error page exception', () => { + it('should handle errors from _error render', async () => { + const { code } = await nextBuild(appDir) + const appPort = await findPort() + const app = await nextStart(appDir, appPort) + const browser = await webdriver(appPort, '/') + await browser.waitForElementByCss(navSel).elementByCss(navSel).click() + const text = await (await browser.elementByCss('#__next')).text() + killApp(app) + + expect(code).toBe(0) + expect(text).toMatch(errorMessage) + }) +})