Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(errors): sync/async error handling for lifecycle hooks and v-on handlers (#7653, #6953) #8395

Merged
merged 8 commits into from
Dec 20, 2018
6 changes: 4 additions & 2 deletions src/core/instance/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
toArray,
hyphenate,
handleError,
formatComponentName
formatComponentName,
handlePromiseError
} from '../util/index'
import { updateListeners } from '../vdom/helpers/index'

Expand Down Expand Up @@ -135,7 +136,8 @@ export function eventsMixin (Vue: Class<Component>) {
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args)
const cbResult = cbs[i].apply(vm, args)
handlePromiseError(cbResult, vm, `event handler for "${event}"`)
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)
}
Expand Down
6 changes: 4 additions & 2 deletions src/core/instance/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
remove,
handleError,
emptyObject,
validateProp
validateProp,
handlePromiseError
} from '../util/index'

export let activeInstance: any = null
Expand Down Expand Up @@ -326,7 +327,8 @@ export function callHook (vm: Component, hook: string) {
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
const fnResult = handlers[i].call(vm)
handlePromiseError(fnResult, vm, `${hook} hook`)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
Expand Down
7 changes: 7 additions & 0 deletions src/core/util/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ 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 (a promise must have a then function)
if (value && typeof value.then === 'function' && typeof value.catch === 'function') {
value.catch(e => handleError(e, vm, info))
}
}

function globalHandleError (err, vm, info) {
if (config.errorHandler) {
try {
Expand Down
23 changes: 17 additions & 6 deletions src/core/vdom/helpers/update-listeners.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* @flow */

import { warn } from 'core/util/index'

import { warn, handleError, handlePromiseError } from 'core/util/index'
import {
cached,
isUndef,
Expand Down Expand Up @@ -31,17 +30,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 @@ -73,7 +84,7 @@ export function updateListeners (
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur)
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
Expand Down
112 changes: 112 additions & 0 deletions test/unit/features/error-handling.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ describe('Error handling', () => {
})
})

// hooks that can return rejected promise
;[
['beforeCreate', 'beforeCreate hook'],
['created', 'created hook'],
['beforeMount', 'beforeMount hook'],
['mounted', 'mounted hook'],
['event', 'event handler for "e"']
].forEach(([type, description]) => {
it(`should recover from promise errors in ${type}`, done => {
createTestInstance(components[`${type}Async`])
waitForUpdate(() => {
expect(`Error in ${description}`).toHaveBeenWarned()
expect(`Error: ${type}`).toHaveBeenWarned()
}).then(done)
})
})

// error in mounted hook should affect neither child nor parent
it('should recover from errors in mounted hook', done => {
const vm = createTestInstance(components.mounted)
Expand All @@ -45,6 +62,20 @@ describe('Error handling', () => {
})
})

// hooks that can return rejected promise
;[
['beforeUpdate', 'beforeUpdate hook'],
['updated', 'updated hook']
].forEach(([type, description]) => {
it(`should recover from promise errors in ${type} hook`, done => {
const vm = createTestInstance(components[`${type}Async`])
assertBothInstancesActive(vm).then(() => {
expect(`Error in ${description}`).toHaveBeenWarned()
expect(`Error: ${type}`).toHaveBeenWarned()
}).then(done)
})
})

;[
['beforeDestroy', 'beforeDestroy hook'],
['destroyed', 'destroyed hook'],
Expand All @@ -62,6 +93,21 @@ describe('Error handling', () => {
})
})

;[
['beforeDestroy', 'beforeDestroy hook'],
['destroyed', 'destroyed hook']
].forEach(([type, description]) => {
it(`should recover from promise errors in ${type} hook`, done => {
const vm = createTestInstance(components[`${type}Async`])
vm.ok = false
setTimeout(() => {
expect(`Error in ${description}`).toHaveBeenWarned()
expect(`Error: ${type}`).toHaveBeenWarned()
assertRootInstanceActive(vm).then(done)
})
})
})

it('should recover from errors in user watcher getter', done => {
const vm = createTestInstance(components.userWatcherGetter)
vm.n++
Expand Down Expand Up @@ -152,6 +198,40 @@ describe('Error handling', () => {
expect(vm.$el.textContent).toContain('error in render')
Vue.config.errorHandler = null
})

// event handlers that can throw errors or return rejected promise
;[
['single handler', '<div v-on:click="bork"></div>'],
['multiple handlers', '<div v-on="{ click: [bork, function test() {}] }"></div>']
].forEach(([type, template]) => {
it(`should recover from v-on errors for ${type} registered`, () => {
const vm = new Vue({
template,
methods: { bork () { throw new Error('v-on') } }
}).$mount()
document.body.appendChild(vm.$el)
triggerEvent(vm.$el, 'click')
expect('Error in v-on').toHaveBeenWarned()
expect('Error: v-on').toHaveBeenWarned()
document.body.removeChild(vm.$el)
})

it(`should recover from v-on async errors for ${type} registered`, (done) => {
const vm = new Vue({
template,
methods: { bork () {
return new Promise((resolve, reject) => reject(new Error('v-on async')))
} }
}).$mount()
document.body.appendChild(vm.$el)
triggerEvent(vm.$el, 'click')
waitForUpdate(() => {
expect('Error in v-on async').toHaveBeenWarned()
expect('Error: v-on async').toHaveBeenWarned()
document.body.removeChild(vm.$el)
}).then(done)
})
})
})

function createErrorTestComponents () {
Expand Down Expand Up @@ -188,6 +268,16 @@ function createErrorTestComponents () {
throw new Error(before)
}

const beforeCompAsync = components[`${before}Async`] = {
props: ['n'],
render (h) {
return h('div', this.n)
}
}
beforeCompAsync[before] = function () {
return new Promise((resolve, reject) => reject(new Error(before)))
}

// after
const after = hook.replace(/e?$/, 'ed')
const afterComp = components[after] = {
Expand All @@ -199,6 +289,16 @@ function createErrorTestComponents () {
afterComp[after] = function () {
throw new Error(after)
}

const afterCompAsync = components[`${after}Async`] = {
props: ['n'],
render (h) {
return h('div', this.n)
}
}
afterCompAsync[after] = function () {
return new Promise((resolve, reject) => reject(new Error(after)))
}
})

// directive hooks errors
Expand Down Expand Up @@ -272,6 +372,18 @@ function createErrorTestComponents () {
}
}

components.eventAsync = {
beforeCreate () {
this.$on('e', () => new Promise((resolve, reject) => reject(new Error('event'))))
},
mounted () {
this.$emit('e')
},
render (h) {
return h('div')
}
}

return components
}

Expand Down