Skip to content

Commit b3cd9bc

Browse files
committed
feat: add catchError option
also propagate error thrown in renderError() to global handler
1 parent e34c6b7 commit b3cd9bc

File tree

5 files changed

+167
-9
lines changed

5 files changed

+167
-9
lines changed

src/core/instance/render.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,21 @@ export function renderMixin (Vue: Class<Component>) {
101101
try {
102102
vnode = render.call(vm._renderProxy, vm.$createElement)
103103
} catch (e) {
104-
handleError(e, vm, `render function`)
104+
handleError(e, vm, `render`)
105105
// return error render result,
106106
// or previous vnode to prevent render error causing blank component
107107
/* istanbul ignore else */
108108
if (process.env.NODE_ENV !== 'production') {
109-
vnode = vm.$options.renderError
110-
? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
111-
: vm._vnode
109+
if (vm.$options.renderError) {
110+
try {
111+
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
112+
} catch (e) {
113+
handleError(e, vm, `renderError`)
114+
vnode = vm._vnode
115+
}
116+
} else {
117+
vnode = vm._vnode
118+
}
112119
} else {
113120
vnode = vm._vnode
114121
}

src/core/util/error.js

+19-3
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,28 @@ import { warn } from './debug'
55
import { inBrowser } from './env'
66

77
export function handleError (err: Error, vm: any, info: string) {
8+
if (vm) {
9+
let cur = vm
10+
while ((cur = cur.$parent)) {
11+
if (cur.$options.catchError) {
12+
try {
13+
const propagate = cur.$options.catchError.call(cur, err, vm, info)
14+
if (!propagate) return
15+
} catch (e) {
16+
globalHandleError(e, cur, 'catchError')
17+
}
18+
}
19+
}
20+
}
21+
globalHandleError(err, vm, info)
22+
}
23+
24+
function globalHandleError (err, vm, info) {
825
if (config.errorHandler) {
926
try {
10-
config.errorHandler.call(null, err, vm, info)
11-
return
27+
return config.errorHandler.call(null, err, vm, info)
1228
} catch (e) {
13-
logError(e, null, 'errorHandler')
29+
logError(e, null, 'config.errorHandler')
1430
}
1531
}
1632
logError(err, vm, info)

test/unit/features/error-handling.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('Error handling', () => {
77
// break parent component
88
;[
99
['data', 'data()'],
10-
['render', 'render function'],
10+
['render', 'render'],
1111
['beforeCreate', 'beforeCreate hook'],
1212
['created', 'created hook'],
1313
['beforeMount', 'beforeMount hook'],
@@ -99,7 +99,7 @@ describe('Error handling', () => {
9999
const args = spy.calls.argsFor(0)
100100
expect(args[0].toString()).toContain('Error: render') // error
101101
expect(args[1]).toBe(vm.$refs.child) // vm
102-
expect(args[2]).toContain('render function') // description
102+
expect(args[2]).toContain('render') // description
103103

104104
assertRootInstanceActive(vm).then(() => {
105105
Vue.config.errorHandler = null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import Vue from 'vue'
2+
3+
describe('Options catchError', () => {
4+
let globalSpy
5+
6+
beforeEach(() => {
7+
globalSpy = Vue.config.errorHandler = jasmine.createSpy()
8+
})
9+
10+
afterEach(() => {
11+
Vue.config.errorHandler = null
12+
})
13+
14+
it('should capture error from child component', () => {
15+
const spy = jasmine.createSpy()
16+
17+
let child
18+
let err
19+
const Child = {
20+
created () {
21+
child = this
22+
err = new Error('child')
23+
throw err
24+
},
25+
render () {}
26+
}
27+
28+
new Vue({
29+
catchError: spy,
30+
render: h => h(Child)
31+
}).$mount()
32+
33+
expect(spy).toHaveBeenCalledWith(err, child, 'created hook')
34+
// should not propagate by default
35+
expect(globalSpy).not.toHaveBeenCalled()
36+
})
37+
38+
it('should be able to render the error in itself', done => {
39+
let child
40+
const Child = {
41+
created () {
42+
child = this
43+
throw new Error('error from child')
44+
},
45+
render () {}
46+
}
47+
48+
const vm = new Vue({
49+
data: {
50+
error: null
51+
},
52+
catchError (e, vm, info) {
53+
expect(vm).toBe(child)
54+
this.error = e.toString() + ' in ' + info
55+
},
56+
render (h) {
57+
if (this.error) {
58+
return h('pre', this.error)
59+
}
60+
return h(Child)
61+
}
62+
}).$mount()
63+
64+
waitForUpdate(() => {
65+
expect(vm.$el.textContent).toContain('error from child')
66+
expect(vm.$el.textContent).toContain('in created hook')
67+
}).then(done)
68+
})
69+
70+
it('should propagate to global handler when returning true', () => {
71+
const spy = jasmine.createSpy()
72+
73+
let child
74+
let err
75+
const Child = {
76+
created () {
77+
child = this
78+
err = new Error('child')
79+
throw err
80+
},
81+
render () {}
82+
}
83+
84+
new Vue({
85+
catchError (err, vm, info) {
86+
spy(err, vm, info)
87+
return true
88+
},
89+
render: h => h(Child, {})
90+
}).$mount()
91+
92+
expect(spy).toHaveBeenCalledWith(err, child, 'created hook')
93+
// should propagate
94+
expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook')
95+
})
96+
97+
it('should propagate to global handler if itself throws error', () => {
98+
let child
99+
let err
100+
const Child = {
101+
created () {
102+
child = this
103+
err = new Error('child')
104+
throw err
105+
},
106+
render () {}
107+
}
108+
109+
let err2
110+
const vm = new Vue({
111+
catchError () {
112+
err2 = new Error('foo')
113+
throw err2
114+
},
115+
render: h => h(Child, {})
116+
}).$mount()
117+
118+
expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook')
119+
expect(globalSpy).toHaveBeenCalledWith(err2, vm, 'catchError')
120+
})
121+
})

test/unit/features/options/renderError.spec.js

+14
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,18 @@ describe('Options renderError', () => {
2525
Vue.config.errorHandler = null
2626
}).then(done)
2727
})
28+
29+
it('should pass on errors in renderError to global handler', () => {
30+
const spy = Vue.config.errorHandler = jasmine.createSpy()
31+
const err = new Error('renderError')
32+
const vm = new Vue({
33+
render () {
34+
throw new Error('render')
35+
},
36+
renderError () {
37+
throw err
38+
}
39+
}).$mount()
40+
expect(spy).toHaveBeenCalledWith(err, vm, 'renderError')
41+
})
2842
})

0 commit comments

Comments
 (0)