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

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

Merged
merged 6 commits into from Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/core/instance/state.js
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
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
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
148 changes: 148 additions & 0 deletions test/unit/features/options/errorCaptured.spec.js
Expand Up @@ -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)
})
})