Skip to content

Commit

Permalink
feat(errors): Catch normal and async errors in v-on handlers
Browse files Browse the repository at this point in the history
Improves the check for promise-like objects to ensure they are thenable.
Includes tests.
  • Loading branch information
coldino committed Jun 27, 2018
1 parent 5309833 commit 3fa6b02
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 8 deletions.
6 changes: 3 additions & 3 deletions src/core/util/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export function handleError (err: Error, vm: any, info: string) {
globalHandleError(err, vm, info)
}

export function handlePromiseError(value: any, vm: any, info: string) {
// if value is promise, handle it
if (value && typeof value.catch === 'function') {
export function handlePromiseError (value: any, vm: any, info: string) {
// if value is promise, handle it (a promise must have a then function)
if (value && typeof value.then === 'function' && typeof value.catch === 'function') {
value.catch(e => handleError(e, vm, info))
}
}
Expand Down
22 changes: 17 additions & 5 deletions src/core/vdom/helpers/update-listeners.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* @flow */

import { warn } from 'core/util/index'
import { warn, handleError, handlePromiseError } from 'core/util/index'
import { cached, isUndef, isPlainObject } from 'shared/util'

const normalizeEvent = cached((name: string): {
Expand All @@ -25,17 +25,29 @@ const normalizeEvent = cached((name: string): {
}
})

export function createFnInvoker (fns: Function | Array<Function>): Function {
export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component): Function {
function invoker () {
const fns = invoker.fns
if (Array.isArray(fns)) {
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
cloned[i].apply(null, arguments)
try {
const result = cloned[i].apply(null, arguments)
handlePromiseError(result, vm, 'v-on async')
} catch (e) {
handleError(e, vm, 'v-on')
}
}
} else {
// return handler return value for single handlers
return fns.apply(null, arguments)
let result
try {
result = fns.apply(null, arguments)
handlePromiseError(result, vm, 'v-on async')
} catch (e) {
handleError(e, vm, 'v-on')
}
return result
}
}
invoker.fns = fns
Expand Down Expand Up @@ -66,7 +78,7 @@ export function updateListeners (
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur)
cur = on[name] = createFnInvoker(cur, vm)
}
add(event.name, cur, event.once, event.capture, event.passive, event.params)
} else if (cur !== old) {
Expand Down
29 changes: 29 additions & 0 deletions test/unit/features/error-handling.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,35 @@ describe('Error handling', () => {
expect(vm.$el.textContent).toContain('error in render')
Vue.config.errorHandler = null
})

it('should capture and recover from v-on errors', () => {
const spy = Vue.config.errorHandler = jasmine.createSpy('errorHandler')
const err1 = new Error('clickbork')
const vm = new Vue({
template: '<div v-on:click="bork"></div>',
methods: { bork: function () { throw err1 } }
}).$mount()
triggerEvent(vm.$el, 'click')
expect(spy.calls.count()).toBe(1)
expect(spy).toHaveBeenCalledWith(err1, vm, 'v-on')
Vue.config.errorHandler = null
})

it('should capture and recover from v-on async errors', (done) => {
const spy = Vue.config.errorHandler = jasmine.createSpy('errorHandler')
const err1 = new Error('clickbork')
const vm = new Vue({
template: '<div v-on:click="bork"></div>',
methods: { bork: function () { return new Promise(function (_resolve, reject) { reject(err1) }) } }
}).$mount()
triggerEvent(vm.$el, 'click')
Vue.nextTick(() => {
expect(spy.calls.count()).toBe(1)
expect(spy).toHaveBeenCalledWith(err1, vm, 'v-on async')
Vue.config.errorHandler = null
done()
})
})
})

function createErrorTestComponents () {
Expand Down

0 comments on commit 3fa6b02

Please sign in to comment.