Skip to content

fix(errorHandler): async error handling for watchers #9484

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

Merged
merged 6 commits into from
Apr 16, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/core/instance/state.js
Original file line number Diff line number Diff line change
@@ -25,7 +25,8 @@ import {
validateProp,
isPlainObject,
isServerRendering,
isReservedAttribute
isReservedAttribute,
invokeWithErrorHandling
} from '../util/index'

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

@@ -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)
}
63 changes: 45 additions & 18 deletions test/unit/features/error-handling.spec.js
Original file line number Diff line number Diff line change
@@ -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 => {
@@ -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 () {
148 changes: 148 additions & 0 deletions test/unit/features/options/errorCaptured.spec.js
Original file line number Diff line number Diff line change
@@ -247,4 +247,152 @@ describe('Options errorCaptured', () => {
expect(store.errors[0]).toEqual(new Error('render error'))
}).then(done)
})

it('should capture error from watcher', done => {
const spy = jasmine.createSpy()

let child
let err
const Child = {
data () {
return {
foo: null
}
},
watch: {
foo () {
err = new Error('userWatcherCallback error')
throw err
}
},
created () {
child = this
},
render () {}
}

new Vue({
errorCaptured: spy,
render: h => h(Child)
}).$mount()

child.foo = 'bar'

waitForUpdate(() => {
expect(spy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo"')
expect(globalSpy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo"')
}).then(done)
})

it('should capture promise error from watcher', done => {
const spy = jasmine.createSpy()

let child
let err
const Child = {
data () {
return {
foo: null
}
},
watch: {
foo () {
err = new Error('userWatcherCallback error')
return Promise.reject(err)
}
},
created () {
child = this
},
render () {}
}

new Vue({
errorCaptured: spy,
render: h => h(Child)
}).$mount()

child.foo = 'bar'

child.$nextTick(() => {
waitForUpdate(() => {
expect(spy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo" (Promise/async)')
expect(globalSpy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo" (Promise/async)')
}).then(done)
})
})

it('should capture error from immediate watcher', done => {
const spy = jasmine.createSpy()

let child
let err
const Child = {
data () {
return {
foo: 'foo'
}
},
watch: {
foo: {
immediate: true,
handler () {
err = new Error('userImmediateWatcherCallback error')
throw err
}
}
},
created () {
child = this
},
render () {}
}

new Vue({
errorCaptured: spy,
render: h => h(Child)
}).$mount()

waitForUpdate(() => {
expect(spy).toHaveBeenCalledWith(err, child, 'callback for immediate watcher "foo"')
expect(globalSpy).toHaveBeenCalledWith(err, child, 'callback for immediate watcher "foo"')
}).then(done)
})

it('should capture promise error from immediate watcher', done => {
const spy = jasmine.createSpy()

let child
let err
const Child = {
data () {
return {
foo: 'foo'
}
},
watch: {
foo: {
immediate: true,
handler () {
err = new Error('userImmediateWatcherCallback error')
return Promise.reject(err)
}
}
},
created () {
child = this
},
render () {}
}

new Vue({
errorCaptured: spy,
render: h => h(Child)
}).$mount()

waitForUpdate(() => {
expect(spy).toHaveBeenCalledWith(err, child, 'callback for immediate watcher "foo" (Promise/async)')
expect(globalSpy).toHaveBeenCalledWith(err, child, 'callback for immediate watcher "foo" (Promise/async)')
}).then(done)
})
})