Browse Source

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

Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com>
dev
hello-brsd 4 years ago
committed by GitHub
parent
commit
e4dea59f84
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      src/core/instance/state.js
  2. 8
      src/core/observer/watcher.js
  3. 63
      test/unit/features/error-handling.spec.js
  4. 148
      test/unit/features/options/errorCaptured.spec.js

10
src/core/instance/state.js

@ -25,7 +25,8 @@ import {
validateProp, validateProp,
isPlainObject, isPlainObject,
isServerRendering, isServerRendering,
isReservedAttribute isReservedAttribute,
invokeWithErrorHandling
} from '../util/index' } from '../util/index'
const sharedPropertyDefinition = { const sharedPropertyDefinition = {
@ -357,12 +358,9 @@ export function stateMixin (Vue: Class<Component>) {
options.user = true options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options) const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) { if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget() pushTarget()
try { invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
popTarget() popTarget()
} }
return function unwatchFn () { return function unwatchFn () {

8
src/core/observer/watcher.js

@ -7,6 +7,7 @@ import {
parsePath, parsePath,
_Set as Set, _Set as Set,
handleError, handleError,
invokeWithErrorHandling,
noop noop
} from '../util/index' } from '../util/index'
@ -191,11 +192,8 @@ export default class Watcher {
const oldValue = this.value const oldValue = this.value
this.value = value this.value = value
if (this.user) { if (this.user) {
try { const info = `callback for watcher "${this.expression}"`
this.cb.call(this.vm, value, oldValue) invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else { } else {
this.cb.call(this.vm, value, oldValue) this.cb.call(this.vm, value, oldValue)
} }

63
test/unit/features/error-handling.spec.js

@ -127,25 +127,25 @@ describe('Error handling', () => {
}).then(done) }).then(done)
}) })
it('should recover from errors in user watcher callback', done => { ;[
const vm = createTestInstance(components.userWatcherCallback) ['userWatcherCallback', 'watcher'],
vm.n++ ['userImmediateWatcherCallback', 'immediate watcher']
waitForUpdate(() => { ].forEach(([type, description]) => {
expect(`Error in callback for watcher "n"`).toHaveBeenWarned() it(`should recover from errors in user ${description} callback`, done => {
expect(`Error: userWatcherCallback`).toHaveBeenWarned() const vm = createTestInstance(components[type])
}).thenWaitFor(next => { assertBothInstancesActive(vm).then(() => {
assertBothInstancesActive(vm).end(next) expect(`Error in callback for ${description} "n"`).toHaveBeenWarned()
}).then(done) expect(`Error: ${type} error`).toHaveBeenWarned()
}) }).then(done)
})
it('should recover from errors in user immediate watcher callback', done => { it(`should recover from promise errors in user ${description} callback`, done => {
const vm = createTestInstance(components.userImmediateWatcherCallback) const vm = createTestInstance(components[`${type}Async`])
waitForUpdate(() => { assertBothInstancesActive(vm).then(() => {
expect(`Error in callback for immediate watcher "n"`).toHaveBeenWarned() expect(`Error in callback for ${description} "n" (Promise/async)`).toHaveBeenWarned()
expect(`Error: userImmediateWatcherCallback error`).toHaveBeenWarned() expect(`Error: ${type} error`).toHaveBeenWarned()
}).thenWaitFor(next => { }).then(done)
assertBothInstancesActive(vm).end(next) })
}).then(done)
}) })
it('config.errorHandler should capture render errors', 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 // event errors
components.event = { components.event = {
beforeCreate () { beforeCreate () {

148
test/unit/features/options/errorCaptured.spec.js

@ -247,4 +247,152 @@ describe('Options errorCaptured', () => {
expect(store.errors[0]).toEqual(new Error('render error')) expect(store.errors[0]).toEqual(new Error('render error'))
}).then(done) }).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)
})
}) })

Loading…
Cancel
Save