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 @@ -131,7 +132,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 @@ -319,7 +320,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
if (value && typeof value.catch === 'function') {
value.catch(e => handleError(e, vm, info))
}
}

function globalHandleError (err, vm, info) {
if (config.errorHandler) {
try {
Expand Down
78 changes: 78 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 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 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 @@ -178,6 +224,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 @@ -189,6 +245,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 @@ -247,6 +313,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