Skip to content

Commit 5fcb496

Browse files
arunodarauchg
authored andcommitted
Introduce better debug error handling (vercel#1592)
* Introduce better debug error handling With this we are rendering runtime and debug errors inside a it's own error root. That gives us better error handling and control. Also, now we are patching React core to capture runtime errors. * Render the initial error on the server.
1 parent 87ff667 commit 5fcb496

5 files changed

Lines changed: 69 additions & 25 deletions

File tree

client/index.js

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createRouter } from '../lib/router'
66
import App from '../lib/app'
77
import evalScript from '../lib/eval-script'
88
import { loadGetInitialProps, getURL } from '../lib/utils'
9+
import ErrorDebugComponent from '../lib/error-debug'
910

1011
// Polyfill Promise globally
1112
// This is needed because Webpack2's dynamic loading(common chunks) code
@@ -39,33 +40,57 @@ export const router = createRouter(pathname, query, getURL(), {
3940
})
4041

4142
const headManager = new HeadManager()
42-
const container = document.getElementById('__next')
43+
const appContainer = document.getElementById('__next')
44+
const errorContainer = document.getElementById('__next-error')
4345

44-
export default (onError) => {
46+
export default () => {
4547
const emitter = mitt()
4648

4749
router.subscribe(({ Component, props, hash, err }) => {
48-
render({ Component, props, err, hash, emitter }, onError)
50+
render({ Component, props, err, hash, emitter })
4951
})
5052

5153
const hash = location.hash.substring(1)
52-
render({ Component, props, hash, err, emitter }, onError)
54+
render({ Component, props, hash, err, emitter })
5355

5456
return emitter
5557
}
5658

57-
export async function render (props, onError = renderErrorComponent) {
59+
export async function render (props) {
60+
if (props.err) {
61+
await renderError(props.err)
62+
return
63+
}
64+
5865
try {
5966
await doRender(props)
6067
} catch (err) {
61-
await onError(err)
68+
if (err.abort) return
69+
await renderError(err)
6270
}
6371
}
6472

65-
async function renderErrorComponent (err) {
66-
const { pathname, query } = router
67-
const props = await loadGetInitialProps(ErrorComponent, { err, pathname, query })
68-
await doRender({ Component: ErrorComponent, props, err })
73+
// This method handles all runtime and debug errors.
74+
// 404 and 500 errors are special kind of errors
75+
// and they are still handle via the main render method.
76+
export async function renderError (error) {
77+
const prod = process.env.NODE_ENV === 'production'
78+
// We need to unmount the current app component because it's
79+
// in the inconsistant state.
80+
// Otherwise, we need to face issues when the issue is fixed and
81+
// it's get notified via HMR
82+
ReactDOM.unmountComponentAtNode(appContainer)
83+
84+
const errorMessage = `${error.message}\n${error.stack}`
85+
console.error(errorMessage)
86+
87+
if (prod) {
88+
const initProps = { err: error, pathname, query }
89+
const props = await loadGetInitialProps(ErrorComponent, initProps)
90+
ReactDOM.render(createElement(ErrorComponent, props), errorContainer)
91+
} else {
92+
ReactDOM.render(createElement(ErrorDebugComponent, { error }), errorContainer)
93+
}
6994
}
7095

7196
async function doRender ({ Component, props, hash, err, emitter }) {
@@ -88,7 +113,9 @@ async function doRender ({ Component, props, hash, err, emitter }) {
88113
// lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
89114
lastAppProps = appProps
90115

91-
ReactDOM.render(createElement(App, appProps), container)
116+
// We need to clear any existing runtime error messages
117+
ReactDOM.unmountComponentAtNode(errorContainer)
118+
ReactDOM.render(createElement(App, appProps), appContainer)
92119

93120
if (emitter) {
94121
emitter.emit('after-reactdom-render', { Component })

client/next-dev.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import evalScript from '../lib/eval-script'
2+
import ReactReconciler from 'react-dom/lib/ReactReconciler'
23

34
const { __NEXT_DATA__: { errorComponent } } = window
45
const ErrorComponent = evalScript(errorComponent).default
@@ -7,12 +8,18 @@ require('react-hot-loader/patch')
78

89
const next = window.next = require('./')
910

10-
const emitter = next.default(onError)
11-
12-
function onError (err) {
13-
// just show the debug screen but don't render ErrorComponent
14-
// so that the current component doesn't lose props
15-
next.render({ err, emitter })
11+
const emitter = next.default()
12+
13+
// This is a patch to catch most of the errors throw inside React components.
14+
const originalMountComponent = ReactReconciler.mountComponent
15+
ReactReconciler.mountComponent = function (...args) {
16+
try {
17+
return originalMountComponent(...args)
18+
} catch (err) {
19+
next.renderError(err)
20+
err.abort = true
21+
throw err
22+
}
1623
}
1724

1825
let lastScroll

lib/app.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default class App extends Component {
1717
}
1818

1919
render () {
20-
const { Component, props, hash, err, router } = this.props
20+
const { Component, props, hash, router } = this.props
2121
const url = createUrl(router)
2222
// If there no component exported we can't proceed.
2323
// We'll tackle that here.
@@ -28,7 +28,6 @@ export default class App extends Component {
2828

2929
return <div>
3030
<Container {...containerProps} />
31-
{ErrorDebug && err ? <ErrorDebug error={err} /> : null}
3231
</div>
3332
}
3433
}

server/document.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import flush from 'styled-jsx/server'
44

55
export default class Document extends Component {
66
static getInitialProps ({ renderPage }) {
7-
const {html, head} = renderPage()
7+
const { html, head, errorHtml } = renderPage()
88
const styles = flush()
9-
return { html, head, styles }
9+
return { html, head, errorHtml, styles }
1010
}
1111

1212
static childContextTypes = {
@@ -49,8 +49,13 @@ export class Main extends Component {
4949
}
5050

5151
render () {
52-
const { html } = this.context._documentProps
53-
return <div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
52+
const { html, errorHtml } = this.context._documentProps
53+
return (
54+
<div>
55+
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
56+
<div id='__next-error' dangerouslySetInnerHTML={{ __html: errorHtml }} />
57+
</div>
58+
)
5459
}
5560
}
5661

server/render.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Router } from '../lib/router'
99
import { loadGetInitialProps } from '../lib/utils'
1010
import Head, { defaultHead } from '../lib/head'
1111
import App from '../lib/app'
12+
import ErrorDebug from '../lib/error-debug'
1213

1314
export async function render (req, res, pathname, query, opts) {
1415
const html = await renderToHTML(req, res, pathname, opts)
@@ -67,20 +68,25 @@ async function doRender (req, res, pathname, query, {
6768
const app = createElement(App, {
6869
Component,
6970
props,
70-
err: dev ? err : null,
7171
router: new Router(pathname, query)
7272
})
7373

7474
const render = staticMarkup ? renderToStaticMarkup : renderToString
7575

7676
let html
7777
let head
78+
let errorHtml = ''
7879
try {
7980
html = render(app)
8081
} finally {
8182
head = Head.rewind() || defaultHead()
8283
}
83-
return { html, head }
84+
85+
if (err && dev) {
86+
errorHtml = render(createElement(ErrorDebug, { error: err }))
87+
}
88+
89+
return { html, head, errorHtml }
8490
}
8591

8692
const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })

0 commit comments

Comments
 (0)