Skip to content

Commit

Permalink
feat: add catchError option
Browse files Browse the repository at this point in the history
also propagate error thrown in renderError() to global handler
  • Loading branch information
yyx990803 committed Oct 5, 2017
1 parent e34c6b7 commit b3cd9bc
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 9 deletions.
15 changes: 11 additions & 4 deletions src/core/instance/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,21 @@ export function renderMixin (Vue: Class<Component>) {
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render function`)
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
vnode = vm.$options.renderError
? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
: vm._vnode
if (vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
Expand Down
22 changes: 19 additions & 3 deletions src/core/util/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,28 @@ import { warn } from './debug'
import { inBrowser } from './env'

export function handleError (err: Error, vm: any, info: string) {
if (vm) {
let cur = vm
while ((cur = cur.$parent)) {
if (cur.$options.catchError) {
try {
const propagate = cur.$options.catchError.call(cur, err, vm, info)
if (!propagate) return
} catch (e) {
globalHandleError(e, cur, 'catchError')
}
}
}
}
globalHandleError(err, vm, info)
}

function globalHandleError (err, vm, info) {
if (config.errorHandler) {
try {
config.errorHandler.call(null, err, vm, info)
return
return config.errorHandler.call(null, err, vm, info)
} catch (e) {
logError(e, null, 'errorHandler')
logError(e, null, 'config.errorHandler')
}
}
logError(err, vm, info)
Expand Down
4 changes: 2 additions & 2 deletions test/unit/features/error-handling.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('Error handling', () => {
// break parent component
;[
['data', 'data()'],
['render', 'render function'],
['render', 'render'],
['beforeCreate', 'beforeCreate hook'],
['created', 'created hook'],
['beforeMount', 'beforeMount hook'],
Expand Down Expand Up @@ -99,7 +99,7 @@ describe('Error handling', () => {
const args = spy.calls.argsFor(0)
expect(args[0].toString()).toContain('Error: render') // error
expect(args[1]).toBe(vm.$refs.child) // vm
expect(args[2]).toContain('render function') // description
expect(args[2]).toContain('render') // description

assertRootInstanceActive(vm).then(() => {
Vue.config.errorHandler = null
Expand Down
121 changes: 121 additions & 0 deletions test/unit/features/options/catchError.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Vue from 'vue'

describe('Options catchError', () => {
let globalSpy

beforeEach(() => {
globalSpy = Vue.config.errorHandler = jasmine.createSpy()
})

afterEach(() => {
Vue.config.errorHandler = null
})

it('should capture error from child component', () => {
const spy = jasmine.createSpy()

let child
let err
const Child = {
created () {
child = this
err = new Error('child')
throw err
},
render () {}
}

new Vue({
catchError: spy,
render: h => h(Child)
}).$mount()

expect(spy).toHaveBeenCalledWith(err, child, 'created hook')
// should not propagate by default
expect(globalSpy).not.toHaveBeenCalled()
})

it('should be able to render the error in itself', done => {
let child
const Child = {
created () {
child = this
throw new Error('error from child')
},
render () {}
}

const vm = new Vue({
data: {
error: null
},
catchError (e, vm, info) {
expect(vm).toBe(child)
this.error = e.toString() + ' in ' + info
},
render (h) {
if (this.error) {
return h('pre', this.error)
}
return h(Child)
}
}).$mount()

waitForUpdate(() => {
expect(vm.$el.textContent).toContain('error from child')
expect(vm.$el.textContent).toContain('in created hook')
}).then(done)
})

it('should propagate to global handler when returning true', () => {
const spy = jasmine.createSpy()

let child
let err
const Child = {
created () {
child = this
err = new Error('child')
throw err
},
render () {}
}

new Vue({
catchError (err, vm, info) {
spy(err, vm, info)
return true
},
render: h => h(Child, {})
}).$mount()

expect(spy).toHaveBeenCalledWith(err, child, 'created hook')
// should propagate
expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook')
})

it('should propagate to global handler if itself throws error', () => {
let child
let err
const Child = {
created () {
child = this
err = new Error('child')
throw err
},
render () {}
}

let err2
const vm = new Vue({
catchError () {
err2 = new Error('foo')
throw err2
},
render: h => h(Child, {})
}).$mount()

expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook')
expect(globalSpy).toHaveBeenCalledWith(err2, vm, 'catchError')
})
})
14 changes: 14 additions & 0 deletions test/unit/features/options/renderError.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,18 @@ describe('Options renderError', () => {
Vue.config.errorHandler = null
}).then(done)
})

it('should pass on errors in renderError to global handler', () => {
const spy = Vue.config.errorHandler = jasmine.createSpy()
const err = new Error('renderError')
const vm = new Vue({
render () {
throw new Error('render')
},
renderError () {
throw err
}
}).$mount()
expect(spy).toHaveBeenCalledWith(err, vm, 'renderError')
})
})

0 comments on commit b3cd9bc

Please sign in to comment.