Skip to content

Commit 3c0cdb5

Browse files
committed
improve error handling for lifecycle hooks
1 parent 3566d92 commit 3c0cdb5

File tree

9 files changed

+254
-67
lines changed

9 files changed

+254
-67
lines changed

Diff for: src/core/instance/lifecycle.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createEmptyVNode } from '../vdom/vnode'
66
import { observerState } from '../observer/index'
77
import { updateComponentListeners } from './events'
88
import { resolveSlots } from './render-helpers/resolve-slots'
9-
import { warn, validateProp, remove, noop, emptyObject } from '../util/index'
9+
import { warn, validateProp, remove, noop, emptyObject, handleError } from '../util/index'
1010

1111
export let activeInstance: any = null
1212

@@ -262,7 +262,11 @@ export function callHook (vm: Component, hook: string) {
262262
const handlers = vm.$options[hook]
263263
if (handlers) {
264264
for (let i = 0, j = handlers.length; i < j; i++) {
265-
handlers[i].call(vm)
265+
try {
266+
handlers[i].call(vm)
267+
} catch (e) {
268+
handleError(e, vm, `${hook} hook`)
269+
}
266270
}
267271
}
268272
if (vm._hasHookEvent) {

Diff for: src/core/instance/render.js

+3-13
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
/* @flow */
22

3-
import config from '../config'
4-
53
import {
64
warn,
75
nextTick,
86
toNumber,
97
_toString,
108
looseEqual,
119
emptyObject,
12-
looseIndexOf,
13-
formatComponentName
10+
handleError,
11+
looseIndexOf
1412
} from '../util/index'
1513

1614
import VNode, {
@@ -79,15 +77,7 @@ export function renderMixin (Vue: Class<Component>) {
7977
try {
8078
vnode = render.call(vm._renderProxy, vm.$createElement)
8179
} catch (e) {
82-
/* istanbul ignore else */
83-
if (config.errorHandler) {
84-
config.errorHandler.call(null, e, vm)
85-
} else {
86-
if (process.env.NODE_ENV !== 'production') {
87-
warn(`Error when rendering ${formatComponentName(vm)}:`)
88-
}
89-
throw e
90-
}
80+
handleError(e, vm, `render function`)
9181
// return previous vnode to prevent render error causing blank component
9282
vnode = vm._vnode
9383
}

Diff for: src/core/observer/watcher.js

+16-14
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
/* @flow */
22

3-
import config from '../config'
4-
import Dep, { pushTarget, popTarget } from './dep'
53
import { queueWatcher } from './scheduler'
4+
import Dep, { pushTarget, popTarget } from './dep'
5+
66
import {
77
warn,
88
remove,
99
isObject,
1010
parsePath,
11-
_Set as Set
11+
_Set as Set,
12+
handleError
1213
} from '../util/index'
1314

1415
let uid = 0
@@ -89,7 +90,17 @@ export default class Watcher {
8990
*/
9091
get () {
9192
pushTarget(this)
92-
const value = this.getter.call(this.vm, this.vm)
93+
let value
94+
const vm = this.vm
95+
if (this.user) {
96+
try {
97+
value = this.getter.call(vm, vm)
98+
} catch (e) {
99+
handleError(e, vm, `getter for watcher "${this.expression}"`)
100+
}
101+
} else {
102+
value = this.getter.call(vm, vm)
103+
}
93104
// "touch" every property so they are all tracked as
94105
// dependencies for deep watching
95106
if (this.deep) {
@@ -172,16 +183,7 @@ export default class Watcher {
172183
try {
173184
this.cb.call(this.vm, value, oldValue)
174185
} catch (e) {
175-
/* istanbul ignore else */
176-
if (config.errorHandler) {
177-
config.errorHandler.call(null, e, this.vm)
178-
} else {
179-
process.env.NODE_ENV !== 'production' && warn(
180-
`Error in watcher "${this.expression}"`,
181-
this.vm
182-
)
183-
throw e
184-
}
186+
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
185187
}
186188
} else {
187189
this.cb.call(this.vm, value, oldValue)

Diff for: src/core/util/error.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import config from '../config'
2+
import { warn } from './debug'
3+
4+
export function handleError (err, vm, type) {
5+
if (config.errorHandler) {
6+
config.errorHandler.call(null, err, vm, type)
7+
} else {
8+
if (process.env.NODE_ENV !== 'production') {
9+
warn(`Error in ${type}:`, vm)
10+
}
11+
if (typeof console !== 'undefined') {
12+
console.error(err)
13+
}
14+
}
15+
}

Diff for: src/core/util/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export * from './env'
44
export * from './options'
55
export * from './debug'
66
export * from './props'
7+
export * from './error'
78
export { defineReactive } from '../observer/index'

Diff for: test/helpers/to-have-been-warned.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ function hasWarned (msg) {
2222
}
2323

2424
function containsMsg (arg) {
25-
if (arg instanceof Error) throw arg
26-
return typeof arg === 'string' && arg.indexOf(msg) > -1
25+
return arg.toString().indexOf(msg) > -1
2726
}
2827
}
2928

@@ -52,7 +51,7 @@ beforeEach(() => {
5251
})
5352

5453
afterEach(done => {
55-
const warned = msg => asserted.some(assertedMsg => msg.indexOf(assertedMsg) > -1)
54+
const warned = msg => asserted.some(assertedMsg => msg.toString().indexOf(assertedMsg) > -1)
5655
let count = console.error.calls.count()
5756
let args
5857
while (count--) {

Diff for: test/helpers/wait-for-update.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Vue from 'vue'
1313
// })
1414
// .then(done)
1515
window.waitForUpdate = initialCb => {
16+
let end
1617
const queue = initialCb ? [initialCb] : []
1718

1819
function shift () {
@@ -33,13 +34,13 @@ window.waitForUpdate = initialCb => {
3334
Vue.nextTick(shift)
3435
}
3536
}
36-
} else if (job && job.fail) {
37+
} else if (job && (job.fail || job === end)) {
3738
job() // done
3839
}
3940
}
4041

4142
Vue.nextTick(() => {
42-
if (!queue.length || !queue[queue.length - 1].fail) {
43+
if (!queue.length || (!end && !queue[queue.length - 1].fail)) {
4344
throw new Error('waitForUpdate chain is missing .then(done)')
4445
}
4546
shift()
@@ -57,6 +58,10 @@ window.waitForUpdate = initialCb => {
5758
wait.wait = true
5859
queue.push(wait)
5960
return chainer
61+
},
62+
end: endFn => {
63+
queue.push(endFn)
64+
end = endFn
6065
}
6166
}
6267

Diff for: test/unit/features/error-handling.spec.js

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import Vue from 'vue'
2+
3+
const components = createErrorTestComponents()
4+
5+
describe('Error handling', () => {
6+
// hooks that prevents the component from rendering, but should not
7+
// break parent component
8+
;[
9+
['render', 'render function'],
10+
['beforeCreate', 'beforeCreate hook'],
11+
['created', 'created hook'],
12+
['beforeMount', 'beforeMount hook']
13+
].forEach(([type, description]) => {
14+
it(`should recover from errors in ${type}`, done => {
15+
const vm = createTestInstance(components[type])
16+
expect(`Error in ${description}`).toHaveBeenWarned()
17+
expect(`Error: ${type}`).toHaveBeenWarned()
18+
assertRootInstanceActive(vm).then(done)
19+
})
20+
})
21+
22+
// error in mounted hook should affect neither child nor parent
23+
it('should recover from errors in mounted hook', done => {
24+
const vm = createTestInstance(components.mounted)
25+
expect(`Error in mounted hook`).toHaveBeenWarned()
26+
expect(`Error: mounted`).toHaveBeenWarned()
27+
assertBothInstancesActive(vm).then(done)
28+
})
29+
30+
// error in beforeUpdate/updated should affect neither child nor parent
31+
;[
32+
['beforeUpdate', 'beforeUpdate hook'],
33+
['updated', 'updated hook']
34+
].forEach(([type, description]) => {
35+
it(`should recover from errors in ${type} hook`, done => {
36+
const vm = createTestInstance(components[type])
37+
assertBothInstancesActive(vm).then(() => {
38+
expect(`Error in ${description}`).toHaveBeenWarned()
39+
expect(`Error: ${type}`).toHaveBeenWarned()
40+
}).then(done)
41+
})
42+
})
43+
44+
;[
45+
['beforeDestroy', 'beforeDestroy hook'],
46+
['destroyed', 'destroyed hook']
47+
].forEach(([type, description]) => {
48+
it(`should recover from errors in ${type} hook`, done => {
49+
const vm = createTestInstance(components[type])
50+
vm.ok = false
51+
waitForUpdate(() => {
52+
expect(`Error in ${description}`).toHaveBeenWarned()
53+
expect(`Error: ${type}`).toHaveBeenWarned()
54+
}).thenWaitFor(next => {
55+
assertRootInstanceActive(vm).end(next)
56+
}).then(done)
57+
})
58+
})
59+
60+
it('should recover from errors in user watcher getter', done => {
61+
const vm = createTestInstance(components.userWatcherGetter)
62+
vm.n++
63+
waitForUpdate(() => {
64+
expect(`Error in getter for watcher`).toHaveBeenWarned()
65+
function getErrorMsg () {
66+
try {
67+
this.a.b.c
68+
} catch (e) {
69+
return e.toString()
70+
}
71+
}
72+
const msg = getErrorMsg.call(vm)
73+
expect(msg).toHaveBeenWarned()
74+
}).thenWaitFor(next => {
75+
assertBothInstancesActive(vm).end(next)
76+
}).then(done)
77+
})
78+
79+
it('should recover from errors in user watcher callback', done => {
80+
const vm = createTestInstance(components.userWatcherCallback)
81+
vm.n++
82+
waitForUpdate(() => {
83+
expect(`Error in callback for watcher "n"`).toHaveBeenWarned()
84+
expect(`Error: userWatcherCallback`).toHaveBeenWarned()
85+
}).thenWaitFor(next => {
86+
assertBothInstancesActive(vm).end(next)
87+
}).then(done)
88+
})
89+
90+
it('config.errorHandler should capture errors', done => {
91+
const spy = Vue.config.errorHandler = jasmine.createSpy('errorHandler')
92+
const vm = createTestInstance(components.render)
93+
94+
const args = spy.calls.argsFor(0)
95+
expect(args[0].toString()).toContain('Error: render') // error
96+
expect(args[1]).toBe(vm.$refs.child) // vm
97+
expect(args[2]).toContain('render function') // description
98+
99+
assertRootInstanceActive(vm).then(done)
100+
})
101+
})
102+
103+
function createErrorTestComponents () {
104+
const components = {}
105+
106+
// render error
107+
components.render = {
108+
render (h) {
109+
throw new Error('render')
110+
}
111+
}
112+
113+
// lifecycle errors
114+
;['create', 'mount', 'update', 'destroy'].forEach(hook => {
115+
// before
116+
const before = 'before' + hook.charAt(0).toUpperCase() + hook.slice(1)
117+
const beforeComp = components[before] = {
118+
props: ['n'],
119+
render (h) {
120+
return h('div', this.n)
121+
}
122+
}
123+
beforeComp[before] = function () {
124+
throw new Error(before)
125+
}
126+
127+
// after
128+
const after = hook.replace(/e?$/, 'ed')
129+
const afterComp = components[after] = {
130+
props: ['n'],
131+
render (h) {
132+
return h('div', this.n)
133+
}
134+
}
135+
afterComp[after] = function () {
136+
throw new Error(after)
137+
}
138+
})
139+
140+
// user watcher
141+
components.userWatcherGetter = {
142+
props: ['n'],
143+
created () {
144+
this.$watch(function () {
145+
return this.n + this.a.b.c
146+
}, val => {
147+
console.log('user watcher fired: ' + val)
148+
})
149+
},
150+
render (h) {
151+
return h('div', this.n)
152+
}
153+
}
154+
155+
components.userWatcherCallback = {
156+
props: ['n'],
157+
watch: {
158+
n () {
159+
throw new Error('userWatcherCallback error')
160+
}
161+
},
162+
render (h) {
163+
return h('div', this.n)
164+
}
165+
}
166+
167+
return components
168+
}
169+
170+
function createTestInstance (Comp) {
171+
return new Vue({
172+
data: {
173+
n: 0,
174+
ok: true
175+
},
176+
render (h) {
177+
return h('div', [
178+
'n:' + this.n + '\n',
179+
this.ok
180+
? h(Comp, { ref: 'child', props: { n: this.n }})
181+
: null
182+
])
183+
}
184+
}).$mount()
185+
}
186+
187+
function assertRootInstanceActive (vm, chain) {
188+
expect(vm.$el.innerHTML).toContain('n:0\n')
189+
vm.n++
190+
return waitForUpdate(() => {
191+
expect(vm.$el.innerHTML).toContain('n:1\n')
192+
})
193+
}
194+
195+
function assertBothInstancesActive (vm) {
196+
vm.n = 0
197+
return waitForUpdate(() => {
198+
expect(vm.$refs.child.$el.innerHTML).toContain('0')
199+
}).thenWaitFor(next => {
200+
assertRootInstanceActive(vm).then(() => {
201+
expect(vm.$refs.child.$el.innerHTML).toContain('1')
202+
}).end(next)
203+
})
204+
}

0 commit comments

Comments
 (0)