Skip to content
10 changes: 4 additions & 6 deletions src/core/instance/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
validateProp,
isPlainObject,
isServerRendering,
isReservedAttribute
isReservedAttribute,
invokeWithErrorHandling
} from '../util/index'

const sharedPropertyDefinition = {
Expand Down Expand Up @@ -355,11 +356,8 @@ export function stateMixin (Vue: Class<Component>) {
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
const info = `callback for immediate watcher "${watcher.expression}"`
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
}
return function unwatchFn () {
watcher.teardown()
Expand Down
8 changes: 3 additions & 5 deletions src/core/observer/watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
parsePath,
_Set as Set,
handleError,
invokeWithErrorHandling,
noop
} from '../util/index'

Expand Down Expand Up @@ -191,11 +192,8 @@ export default class Watcher {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
Expand Down
63 changes: 45 additions & 18 deletions test/unit/features/error-handling.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,25 +127,25 @@ describe('Error handling', () => {
}).then(done)
})

it('should recover from errors in user watcher callback', done => {
const vm = createTestInstance(components.userWatcherCallback)
vm.n++
waitForUpdate(() => {
expect(`Error in callback for watcher "n"`).toHaveBeenWarned()
expect(`Error: userWatcherCallback`).toHaveBeenWarned()
}).thenWaitFor(next => {
assertBothInstancesActive(vm).end(next)
}).then(done)
})
;[
['userWatcherCallback', 'watcher'],
['userImmediateWatcherCallback', 'immediate watcher']
].forEach(([type, description]) => {
it(`should recover from errors in user ${description} callback`, done => {
const vm = createTestInstance(components[type])
assertBothInstancesActive(vm).then(() => {
expect(`Error in callback for ${description} "n"`).toHaveBeenWarned()
expect(`Error: ${type} error`).toHaveBeenWarned()
}).then(done)
})

it('should recover from errors in user immediate watcher callback', done => {
const vm = createTestInstance(components.userImmediateWatcherCallback)
waitForUpdate(() => {
expect(`Error in callback for immediate watcher "n"`).toHaveBeenWarned()
expect(`Error: userImmediateWatcherCallback error`).toHaveBeenWarned()
}).thenWaitFor(next => {
assertBothInstancesActive(vm).end(next)
}).then(done)
it(`should recover from promise errors in user ${description} callback`, done => {
const vm = createTestInstance(components[`${type}Async`])
assertBothInstancesActive(vm).then(() => {
expect(`Error in callback for ${description} "n" (Promise/async)`).toHaveBeenWarned()
expect(`Error: ${type} error`).toHaveBeenWarned()
}).then(done)
})
})

it('config.errorHandler should capture render errors', done => {
Expand Down Expand Up @@ -359,6 +359,33 @@ function createErrorTestComponents () {
}
}

components.userWatcherCallbackAsync = {
props: ['n'],
watch: {
n () {
return Promise.reject(new Error('userWatcherCallback error'))
}
},
render (h) {
return h('div', this.n)
}
}

components.userImmediateWatcherCallbackAsync = {
props: ['n'],
watch: {
n: {
immediate: true,
handler () {
return Promise.reject(new Error('userImmediateWatcherCallback error'))
}
}
},
render (h) {
return h('div', this.n)
}
}

// event errors
components.event = {
beforeCreate () {
Expand Down