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:
parent
87ff667e6e
commit
5fcb49632e
5 changed files with 68 additions and 24 deletions
|
@ -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 })
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
Loading…
Reference in a new issue