Skip to content

Commit

Permalink
Handle runtime errors (#268)
Browse files Browse the repository at this point in the history
* display runtime errors by error-debug

* server: fix status

* render Error component on client error

* server: render runtime errors of error template

* server: handle errors of error template on render404

* server: add a comment

* server: refactor renderJSON

* recover from runtime errors

* _error: check if xhr exists

* _error: improve client error

* _error: improve error message
  • Loading branch information
nkzawa committed Nov 24, 2016
1 parent a14cc66 commit c7ba914
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 85 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,17 @@ import React from 'react'

export default class Error extends React.Component {
static getInitialProps ({ res, xhr }) {
const statusCode = res ? res.statusCode : xhr.status
const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)
return { statusCode }
}

render () {
return (
<p>An error { this.props.statusCode } occurred</p>
<p>{
this.props.statusCode
? `An error ${this.props.statusCode} occurred on server`
: 'An error occurred on client'
]</p>
)
}
}
Expand Down
9 changes: 7 additions & 2 deletions client/next.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import App from '../lib/app'
import evalScript from '../lib/eval-script'

const {
__NEXT_DATA__: { component, props, ids, err }
__NEXT_DATA__: { component, errorComponent, props, ids, err }
} = window

const Component = evalScript(component).default
const ErrorComponent = evalScript(errorComponent).default

export const router = new Router(window.location.href, { Component, ctx: { err } })
export const router = new Router(window.location.href, {
Component,
ErrorComponent,
ctx: { err }
})

const headManager = new HeadManager()
const container = document.getElementById('__next')
Expand Down
7 changes: 7 additions & 0 deletions client/webpack-hot-middleware-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const handlers = {
}

next.router.reload(route)
},
change (route) {
const { Component } = next.router.components[route] || {}
if (Component && Component.__route === '/_error-debug') {
// reload to recover from runtime errors
next.router.reload(route)
}
}
}

Expand Down
44 changes: 35 additions & 9 deletions lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import evalScript from './eval-script'
import shallowEquals from './shallow-equals'

export default class Router {
constructor (url, initialData) {
constructor (url, { Component, ErrorComponent, ctx } = {}) {
const parsed = parse(url, true)

// represents the current component key
this.route = toRoute(parsed.pathname)

// set up the component cache (by route keys)
this.components = { [this.route]: initialData }
this.components = { [this.route]: { Component, ctx } }

this.ErrorComponent = ErrorComponent
this.pathname = parsed.pathname
this.query = parsed.query
this.subscriptions = new Set()
Expand All @@ -38,13 +39,19 @@ export default class Router {
this.route = route
this.set(getURL(), { ...data, props })
})
.catch((err) => {
.catch(async (err) => {
if (err.cancelled) return

// the only way we can appropriately handle
// this failure is deferring to the browser
// since the URL has already changed
window.location.reload()
const data = { Component: this.ErrorComponent, ctx: { err } }
const ctx = { ...data.ctx, pathname, query }
const props = await this.getInitialProps(data.Component, ctx)

this.route = route
this.set(getURL(), { ...data, props })
console.error(err)
})
.catch((err) => {
console.error(err)
})
}

Expand All @@ -67,16 +74,25 @@ export default class Router {

let data
let props
let _err
try {
data = await this.fetchComponent(route)
const ctx = { ...data.ctx, pathname, query }
props = await this.getInitialProps(data.Component, ctx)
} catch (err) {
if (err.cancelled) return false
throw err

data = { Component: this.ErrorComponent, ctx: { err } }
const ctx = { ...data.ctx, pathname, query }
props = await this.getInitialProps(data.Component, ctx)

_err = err
console.error(err)
}

this.notify({ ...data, props })

if (_err) throw _err
}

back () {
Expand All @@ -100,13 +116,20 @@ export default class Router {

let data
let props
let _err
try {
data = await this.fetchComponent(route)
const ctx = { ...data.ctx, pathname, query }
props = await this.getInitialProps(data.Component, ctx)
} catch (err) {
if (err.cancelled) return false
throw err

data = { Component: this.ErrorComponent, ctx: { err } }
const ctx = { ...data.ctx, pathname, query }
props = await this.getInitialProps(data.Component, ctx)

_err = err
console.error(err)
}

if (getURL() !== url) {
Expand All @@ -115,6 +138,9 @@ export default class Router {

this.route = route
this.set(url, { ...data, props })

if (_err) throw _err

return true
}

Expand Down
14 changes: 9 additions & 5 deletions pages/_error-debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import style from 'next/css'

export default class ErrorDebug extends React.Component {
static getInitialProps ({ err }) {
const { message, module } = err
return { message, path: module.rawRequest }
const { name, message, stack, module } = err
return { name, message, stack, path: module ? module.rawRequest : null }
}

render () {
const { message, path } = this.props
const { name, message, stack, path } = this.props

return <div className={styles.errorDebug}>
<Head>
Expand All @@ -21,8 +21,12 @@ export default class ErrorDebug extends React.Component {
}
`}} />
</Head>
<div className={styles.heading}>Error in {path}</div>
<pre className={styles.message} dangerouslySetInnerHTML={{ __html: ansiHTML(encodeHtml(message)) }} />
{path ? <div className={styles.heading}>Error in {path}</div> : null}
{
name === 'ModuleBuildError'
? <pre className={styles.message} dangerouslySetInnerHTML={{ __html: ansiHTML(encodeHtml(message)) }} />
: <pre className={styles.message}>{stack}</pre>
}
</div>
}
}
Expand Down
12 changes: 7 additions & 5 deletions pages/_error.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import React from 'react'
import style, { merge } from 'next/css'
import style from 'next/css'

export default class Error extends React.Component {
static getInitialProps ({ res, xhr }) {
const statusCode = res ? res.statusCode : xhr.status
const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)
return { statusCode }
}

render () {
const { statusCode } = this.props
const title = statusCode === 404 ? 'This page could not be found' : 'Internal Server Error'
const title = statusCode === 404
? 'This page could not be found'
: (statusCode ? 'Internal Server Error' : 'An unexpected error has occurred')

return <div className={merge(styles.error, styles['error_' + statusCode])}>
return <div className={styles.error}>
<div className={styles.text}>
<h1 className={styles.h1}>{statusCode}</h1>
{statusCode ? <h1 className={styles.h1}>{statusCode}</h1> : null}
<div className={styles.desc}>
<h2 className={styles.h2}>{title}.</h2>
</div>
Expand Down
14 changes: 14 additions & 0 deletions server/hot-reloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class HotReloader {
this.prevAssets = null
this.prevChunkNames = null
this.prevFailedChunkNames = null
this.prevChunkHashes = null
}

async run (req, res) {
Expand Down Expand Up @@ -66,6 +67,8 @@ export default class HotReloader {
.reduce((a, b) => a.concat(b), [])
.map((c) => c.name))

const chunkHashes = new Map(compilation.chunks.map((c) => [c.name, c.hash]))

if (this.initialized) {
// detect chunks which have to be replaced with a new template
// e.g, pages/index.js <-> pages/_error.js
Expand All @@ -83,13 +86,24 @@ export default class HotReloader {
const route = toRoute(relative(rootDir, n))
this.send('reload', route)
}

for (const [n, hash] of chunkHashes) {
if (!this.prevChunkHashes.has(n)) continue
if (this.prevChunkHashes.get(n) === hash) continue

const route = toRoute(relative(rootDir, n))

// notify change to recover from runtime errors
this.send('change', route)
}
}

this.initialized = true
this.stats = stats
this.compilationErrors = null
this.prevChunkNames = chunkNames
this.prevFailedChunkNames = failedChunkNames
this.prevChunkHashes = chunkHashes
})

this.webpackDevMiddleware = webpackDevMiddleware(compiler, {
Expand Down
Loading

0 comments on commit c7ba914

Please sign in to comment.