Skip to content

Commit 6e9fcfc

Browse files
enkotyyx990803
authored andcommitted
feat(errors): sync/async error handling for lifecycle hooks and v-on handlers (#8395)
close #6953, close #7653
1 parent b7f7f27 commit 6e9fcfc

File tree

5 files changed

+144
-10
lines changed

5 files changed

+144
-10
lines changed

src/core/instance/events.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
toArray,
66
hyphenate,
77
handleError,
8-
formatComponentName
8+
formatComponentName,
9+
handlePromiseError
910
} from '../util/index'
1011
import { updateListeners } from '../vdom/helpers/index'
1112

@@ -135,7 +136,8 @@ export function eventsMixin (Vue: Class<Component>) {
135136
const args = toArray(arguments, 1)
136137
for (let i = 0, l = cbs.length; i < l; i++) {
137138
try {
138-
cbs[i].apply(vm, args)
139+
const cbResult = cbs[i].apply(vm, args)
140+
handlePromiseError(cbResult, vm, `event handler for "${event}"`)
139141
} catch (e) {
140142
handleError(e, vm, `event handler for "${event}"`)
141143
}

src/core/instance/lifecycle.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
remove,
1616
handleError,
1717
emptyObject,
18-
validateProp
18+
validateProp,
19+
handlePromiseError
1920
} from '../util/index'
2021

2122
export let activeInstance: any = null
@@ -326,7 +327,8 @@ export function callHook (vm: Component, hook: string) {
326327
if (handlers) {
327328
for (let i = 0, j = handlers.length; i < j; i++) {
328329
try {
329-
handlers[i].call(vm)
330+
const fnResult = handlers[i].call(vm)
331+
handlePromiseError(fnResult, vm, `${hook} hook`)
330332
} catch (e) {
331333
handleError(e, vm, `${hook} hook`)
332334
}

src/core/util/error.js

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ export function handleError (err: Error, vm: any, info: string) {
2424
globalHandleError(err, vm, info)
2525
}
2626

27+
export function handlePromiseError (value: any, vm: any, info: string) {
28+
// if value is promise, handle it (a promise must have a then function)
29+
if (value && typeof value.then === 'function' && typeof value.catch === 'function') {
30+
value.catch(e => handleError(e, vm, info))
31+
}
32+
}
33+
2734
function globalHandleError (err, vm, info) {
2835
if (config.errorHandler) {
2936
try {

src/core/vdom/helpers/update-listeners.js

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* @flow */
22

3-
import { warn } from 'core/util/index'
4-
3+
import { warn, handleError, handlePromiseError } from 'core/util/index'
54
import {
65
cached,
76
isUndef,
@@ -31,17 +30,29 @@ const normalizeEvent = cached((name: string): {
3130
}
3231
})
3332

34-
export function createFnInvoker (fns: Function | Array<Function>): Function {
33+
export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component): Function {
3534
function invoker () {
3635
const fns = invoker.fns
3736
if (Array.isArray(fns)) {
3837
const cloned = fns.slice()
3938
for (let i = 0; i < cloned.length; i++) {
40-
cloned[i].apply(null, arguments)
39+
try {
40+
const result = cloned[i].apply(null, arguments)
41+
handlePromiseError(result, vm, 'v-on async')
42+
} catch (e) {
43+
handleError(e, vm, 'v-on')
44+
}
4145
}
4246
} else {
4347
// return handler return value for single handlers
44-
return fns.apply(null, arguments)
48+
let result
49+
try {
50+
result = fns.apply(null, arguments)
51+
handlePromiseError(result, vm, 'v-on async')
52+
} catch (e) {
53+
handleError(e, vm, 'v-on')
54+
}
55+
return result
4556
}
4657
}
4758
invoker.fns = fns
@@ -73,7 +84,7 @@ export function updateListeners (
7384
)
7485
} else if (isUndef(old)) {
7586
if (isUndef(cur.fns)) {
76-
cur = on[name] = createFnInvoker(cur)
87+
cur = on[name] = createFnInvoker(cur, vm)
7788
}
7889
if (isTrue(event.once)) {
7990
cur = on[name] = createOnceHandler(event.name, cur, event.capture)

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

+112
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ describe('Error handling', () => {
2222
})
2323
})
2424

25+
// hooks that can return rejected promise
26+
;[
27+
['beforeCreate', 'beforeCreate hook'],
28+
['created', 'created hook'],
29+
['beforeMount', 'beforeMount hook'],
30+
['mounted', 'mounted hook'],
31+
['event', 'event handler for "e"']
32+
].forEach(([type, description]) => {
33+
it(`should recover from promise errors in ${type}`, done => {
34+
createTestInstance(components[`${type}Async`])
35+
waitForUpdate(() => {
36+
expect(`Error in ${description}`).toHaveBeenWarned()
37+
expect(`Error: ${type}`).toHaveBeenWarned()
38+
}).then(done)
39+
})
40+
})
41+
2542
// error in mounted hook should affect neither child nor parent
2643
it('should recover from errors in mounted hook', done => {
2744
const vm = createTestInstance(components.mounted)
@@ -45,6 +62,20 @@ describe('Error handling', () => {
4562
})
4663
})
4764

65+
// hooks that can return rejected promise
66+
;[
67+
['beforeUpdate', 'beforeUpdate hook'],
68+
['updated', 'updated hook']
69+
].forEach(([type, description]) => {
70+
it(`should recover from promise errors in ${type} hook`, done => {
71+
const vm = createTestInstance(components[`${type}Async`])
72+
assertBothInstancesActive(vm).then(() => {
73+
expect(`Error in ${description}`).toHaveBeenWarned()
74+
expect(`Error: ${type}`).toHaveBeenWarned()
75+
}).then(done)
76+
})
77+
})
78+
4879
;[
4980
['beforeDestroy', 'beforeDestroy hook'],
5081
['destroyed', 'destroyed hook'],
@@ -62,6 +93,21 @@ describe('Error handling', () => {
6293
})
6394
})
6495

96+
;[
97+
['beforeDestroy', 'beforeDestroy hook'],
98+
['destroyed', 'destroyed hook']
99+
].forEach(([type, description]) => {
100+
it(`should recover from promise errors in ${type} hook`, done => {
101+
const vm = createTestInstance(components[`${type}Async`])
102+
vm.ok = false
103+
setTimeout(() => {
104+
expect(`Error in ${description}`).toHaveBeenWarned()
105+
expect(`Error: ${type}`).toHaveBeenWarned()
106+
assertRootInstanceActive(vm).then(done)
107+
})
108+
})
109+
})
110+
65111
it('should recover from errors in user watcher getter', done => {
66112
const vm = createTestInstance(components.userWatcherGetter)
67113
vm.n++
@@ -152,6 +198,40 @@ describe('Error handling', () => {
152198
expect(vm.$el.textContent).toContain('error in render')
153199
Vue.config.errorHandler = null
154200
})
201+
202+
// event handlers that can throw errors or return rejected promise
203+
;[
204+
['single handler', '<div v-on:click="bork"></div>'],
205+
['multiple handlers', '<div v-on="{ click: [bork, function test() {}] }"></div>']
206+
].forEach(([type, template]) => {
207+
it(`should recover from v-on errors for ${type} registered`, () => {
208+
const vm = new Vue({
209+
template,
210+
methods: { bork () { throw new Error('v-on') } }
211+
}).$mount()
212+
document.body.appendChild(vm.$el)
213+
triggerEvent(vm.$el, 'click')
214+
expect('Error in v-on').toHaveBeenWarned()
215+
expect('Error: v-on').toHaveBeenWarned()
216+
document.body.removeChild(vm.$el)
217+
})
218+
219+
it(`should recover from v-on async errors for ${type} registered`, (done) => {
220+
const vm = new Vue({
221+
template,
222+
methods: { bork () {
223+
return new Promise((resolve, reject) => reject(new Error('v-on async')))
224+
} }
225+
}).$mount()
226+
document.body.appendChild(vm.$el)
227+
triggerEvent(vm.$el, 'click')
228+
waitForUpdate(() => {
229+
expect('Error in v-on async').toHaveBeenWarned()
230+
expect('Error: v-on async').toHaveBeenWarned()
231+
document.body.removeChild(vm.$el)
232+
}).then(done)
233+
})
234+
})
155235
})
156236

157237
function createErrorTestComponents () {
@@ -188,6 +268,16 @@ function createErrorTestComponents () {
188268
throw new Error(before)
189269
}
190270

271+
const beforeCompAsync = components[`${before}Async`] = {
272+
props: ['n'],
273+
render (h) {
274+
return h('div', this.n)
275+
}
276+
}
277+
beforeCompAsync[before] = function () {
278+
return new Promise((resolve, reject) => reject(new Error(before)))
279+
}
280+
191281
// after
192282
const after = hook.replace(/e?$/, 'ed')
193283
const afterComp = components[after] = {
@@ -199,6 +289,16 @@ function createErrorTestComponents () {
199289
afterComp[after] = function () {
200290
throw new Error(after)
201291
}
292+
293+
const afterCompAsync = components[`${after}Async`] = {
294+
props: ['n'],
295+
render (h) {
296+
return h('div', this.n)
297+
}
298+
}
299+
afterCompAsync[after] = function () {
300+
return new Promise((resolve, reject) => reject(new Error(after)))
301+
}
202302
})
203303

204304
// directive hooks errors
@@ -272,6 +372,18 @@ function createErrorTestComponents () {
272372
}
273373
}
274374

375+
components.eventAsync = {
376+
beforeCreate () {
377+
this.$on('e', () => new Promise((resolve, reject) => reject(new Error('event'))))
378+
},
379+
mounted () {
380+
this.$emit('e')
381+
},
382+
render (h) {
383+
return h('div')
384+
}
385+
}
386+
275387
return components
276388
}
277389

0 commit comments

Comments
 (0)