Introduce better debug error handling (#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.
This commit is contained in:
Arunoda Susiripala 2017-04-02 02:33:40 +05:30 committed by Guillermo Rauch
parent 87ff667e6e
commit 5fcb49632e
5 changed files with 68 additions and 24 deletions

View file

@ -6,6 +6,7 @@ import { createRouter } from '../lib/router'
import App from '../lib/app' import App from '../lib/app'
import evalScript from '../lib/eval-script' import evalScript from '../lib/eval-script'
import { loadGetInitialProps, getURL } from '../lib/utils' import { loadGetInitialProps, getURL } from '../lib/utils'
import ErrorDebugComponent from '../lib/error-debug'
// Polyfill Promise globally // Polyfill Promise globally
// This is needed because Webpack2's dynamic loading(common chunks) code // This is needed because Webpack2's dynamic loading(common chunks) code
@ -39,33 +40,57 @@ export const router = createRouter(pathname, query, getURL(), {
}) })
const headManager = new HeadManager() const headManager = new HeadManager()
const container = document.getElementById('__next') const appContainer = document.getElementById('__next')
const errorContainer = document.getElementById('__next-error')
export default (onError) => { export default () => {
const emitter = mitt() const emitter = mitt()
router.subscribe(({ Component, props, hash, err }) => { router.subscribe(({ Component, props, hash, err }) => {
render({ Component, props, err, hash, emitter }, onError) render({ Component, props, err, hash, emitter })
}) })
const hash = location.hash.substring(1) const hash = location.hash.substring(1)
render({ Component, props, hash, err, emitter }, onError) render({ Component, props, hash, err, emitter })
return emitter return emitter
} }
export async function render (props, onError = renderErrorComponent) { export async function render (props) {
if (props.err) {
await renderError(props.err)
return
}
try { try {
await doRender(props) await doRender(props)
} catch (err) { } catch (err) {
await onError(err) if (err.abort) return
await renderError(err)
} }
} }
async function renderErrorComponent (err) { // This method handles all runtime and debug errors.
const { pathname, query } = router // 404 and 500 errors are special kind of errors
const props = await loadGetInitialProps(ErrorComponent, { err, pathname, query }) // and they are still handle via the main render method.
await doRender({ Component: ErrorComponent, props, err }) export async function renderError (error) {
const prod = process.env.NODE_ENV === 'production'
// We need to unmount the current app component because it's
// in the inconsistant state.
// Otherwise, we need to face issues when the issue is fixed and
// it's get notified via HMR
ReactDOM.unmountComponentAtNode(appContainer)
const errorMessage = `${error.message}\n${error.stack}`
console.error(errorMessage)
if (prod) {
const initProps = { err: error, pathname, query }
const props = await loadGetInitialProps(ErrorComponent, initProps)
ReactDOM.render(createElement(ErrorComponent, props), errorContainer)
} else {
ReactDOM.render(createElement(ErrorDebugComponent, { error }), errorContainer)
}
} }
async function doRender ({ Component, props, hash, err, emitter }) { async function doRender ({ Component, props, hash, err, emitter }) {
@ -88,7 +113,9 @@ async function doRender ({ Component, props, hash, err, emitter }) {
// lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error. // lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
lastAppProps = appProps lastAppProps = appProps
ReactDOM.render(createElement(App, appProps), container) // We need to clear any existing runtime error messages
ReactDOM.unmountComponentAtNode(errorContainer)
ReactDOM.render(createElement(App, appProps), appContainer)
if (emitter) { if (emitter) {
emitter.emit('after-reactdom-render', { Component }) emitter.emit('after-reactdom-render', { Component })

View file

@ -1,4 +1,5 @@
import evalScript from '../lib/eval-script' import evalScript from '../lib/eval-script'
import ReactReconciler from 'react-dom/lib/ReactReconciler'
const { __NEXT_DATA__: { errorComponent } } = window const { __NEXT_DATA__: { errorComponent } } = window
const ErrorComponent = evalScript(errorComponent).default const ErrorComponent = evalScript(errorComponent).default
@ -7,12 +8,18 @@ require('react-hot-loader/patch')
const next = window.next = require('./') const next = window.next = require('./')
const emitter = next.default(onError) const emitter = next.default()
function onError (err) { // This is a patch to catch most of the errors throw inside React components.
// just show the debug screen but don't render ErrorComponent const originalMountComponent = ReactReconciler.mountComponent
// so that the current component doesn't lose props ReactReconciler.mountComponent = function (...args) {
next.render({ err, emitter }) try {
return originalMountComponent(...args)
} catch (err) {
next.renderError(err)
err.abort = true
throw err
}
} }
let lastScroll let lastScroll

View file

@ -17,7 +17,7 @@ export default class App extends Component {
} }
render () { render () {
const { Component, props, hash, err, router } = this.props const { Component, props, hash, router } = this.props
const url = createUrl(router) const url = createUrl(router)
// If there no component exported we can't proceed. // If there no component exported we can't proceed.
// We'll tackle that here. // We'll tackle that here.
@ -28,7 +28,6 @@ export default class App extends Component {
return <div> return <div>
<Container {...containerProps} /> <Container {...containerProps} />
{ErrorDebug && err ? <ErrorDebug error={err} /> : null}
</div> </div>
} }
} }

View file

@ -4,9 +4,9 @@ import flush from 'styled-jsx/server'
export default class Document extends Component { export default class Document extends Component {
static getInitialProps ({ renderPage }) { static getInitialProps ({ renderPage }) {
const {html, head} = renderPage() const { html, head, errorHtml } = renderPage()
const styles = flush() const styles = flush()
return { html, head, styles } return { html, head, errorHtml, styles }
} }
static childContextTypes = { static childContextTypes = {
@ -49,8 +49,13 @@ export class Main extends Component {
} }
render () { render () {
const { html } = this.context._documentProps const { html, errorHtml } = this.context._documentProps
return <div id='__next' dangerouslySetInnerHTML={{ __html: html }} /> return (
<div>
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
<div id='__next-error' dangerouslySetInnerHTML={{ __html: errorHtml }} />
</div>
)
} }
} }

View file

@ -9,6 +9,7 @@ import { Router } from '../lib/router'
import { loadGetInitialProps } from '../lib/utils' import { loadGetInitialProps } from '../lib/utils'
import Head, { defaultHead } from '../lib/head' import Head, { defaultHead } from '../lib/head'
import App from '../lib/app' import App from '../lib/app'
import ErrorDebug from '../lib/error-debug'
export async function render (req, res, pathname, query, opts) { export async function render (req, res, pathname, query, opts) {
const html = await renderToHTML(req, res, pathname, opts) const html = await renderToHTML(req, res, pathname, opts)
@ -67,7 +68,6 @@ async function doRender (req, res, pathname, query, {
const app = createElement(App, { const app = createElement(App, {
Component, Component,
props, props,
err: dev ? err : null,
router: new Router(pathname, query) router: new Router(pathname, query)
}) })
@ -75,12 +75,18 @@ async function doRender (req, res, pathname, query, {
let html let html
let head let head
let errorHtml = ''
try { try {
html = render(app) html = render(app)
} finally { } finally {
head = Head.rewind() || defaultHead() head = Head.rewind() || defaultHead()
} }
return { html, head }
if (err && dev) {
errorHtml = render(createElement(ErrorDebug, { error: err }))
}
return { html, head, errorHtml }
} }
const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage }) const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })