Skip to content

Commit e4dea59

Browse files
hello-brsdposva
andauthoredApr 16, 2021
fix(errorHandler): async error handling for watchers (#9484)
Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com>
1 parent 3ad60fe commit e4dea59

File tree

4 files changed

+200
-29
lines changed

4 files changed

+200
-29
lines changed
 

‎src/core/instance/state.js

+4-6
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
validateProp,
2626
isPlainObject,
2727
isServerRendering,
28-
isReservedAttribute
28+
isReservedAttribute,
29+
invokeWithErrorHandling
2930
} from '../util/index'
3031

3132
const sharedPropertyDefinition = {
@@ -357,12 +358,9 @@ export function stateMixin (Vue: Class<Component>) {
357358
options.user = true
358359
const watcher = new Watcher(vm, expOrFn, cb, options)
359360
if (options.immediate) {
361+
const info = `callback for immediate watcher "${watcher.expression}"`
360362
pushTarget()
361-
try {
362-
cb.call(vm, watcher.value)
363-
} catch (error) {
364-
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
365-
}
363+
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
366364
popTarget()
367365
}
368366
return function unwatchFn () {

‎src/core/observer/watcher.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
parsePath,
88
_Set as Set,
99
handleError,
10+
invokeWithErrorHandling,
1011
noop
1112
} from '../util/index'
1213

@@ -191,11 +192,8 @@ export default class Watcher {
191192
const oldValue = this.value
192193
this.value = value
193194
if (this.user) {
194-
try {
195-
this.cb.call(this.vm, value, oldValue)
196-
} catch (e) {
197-
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
198-
}
195+
const info = `callback for watcher "${this.expression}"`
196+
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
199197
} else {
200198
this.cb.call(this.vm, value, oldValue)
201199
}

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

+45-18
Original file line numberDiff line numberDiff line change
@@ -127,25 +127,25 @@ describe('Error handling', () => {
127127
}).then(done)
128128
})
129129

130-
it('should recover from errors in user watcher callback', done => {
131-
const vm = createTestInstance(components.userWatcherCallback)
132-
vm.n++
133-
waitForUpdate(() => {
134-
expect(`Error in callback for watcher "n"`).toHaveBeenWarned()
135-
expect(`Error: userWatcherCallback`).toHaveBeenWarned()
136-
}).thenWaitFor(next => {
137-
assertBothInstancesActive(vm).end(next)
138-
}).then(done)
139-
})
130+
;[
131+
['userWatcherCallback', 'watcher'],
132+
['userImmediateWatcherCallback', 'immediate watcher']
133+
].forEach(([type, description]) => {
134+
it(`should recover from errors in user ${description} callback`, done => {
135+
const vm = createTestInstance(components[type])
136+
assertBothInstancesActive(vm).then(() => {
137+
expect(`Error in callback for ${description} "n"`).toHaveBeenWarned()
138+
expect(`Error: ${type} error`).toHaveBeenWarned()
139+
}).then(done)
140+
})
140141

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

151151
it('config.errorHandler should capture render errors', done => {
@@ -359,6 +359,33 @@ function createErrorTestComponents () {
359359
}
360360
}
361361

362+
components.userWatcherCallbackAsync = {
363+
props: ['n'],
364+
watch: {
365+
n () {
366+
return Promise.reject(new Error('userWatcherCallback error'))
367+
}
368+
},
369+
render (h) {
370+
return h('div', this.n)
371+
}
372+
}
373+
374+
components.userImmediateWatcherCallbackAsync = {
375+
props: ['n'],
376+
watch: {
377+
n: {
378+
immediate: true,
379+
handler () {
380+
return Promise.reject(new Error('userImmediateWatcherCallback error'))
381+
}
382+
}
383+
},
384+
render (h) {
385+
return h('div', this.n)
386+
}
387+
}
388+
362389
// event errors
363390
components.event = {
364391
beforeCreate () {

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

+148
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,152 @@ describe('Options errorCaptured', () => {
247247
expect(store.errors[0]).toEqual(new Error('render error'))
248248
}).then(done)
249249
})
250+
251+
it('should capture error from watcher', done => {
252+
const spy = jasmine.createSpy()
253+
254+
let child
255+
let err
256+
const Child = {
257+
data () {
258+
return {
259+
foo: null
260+
}
261+
},
262+
watch: {
263+
foo () {
264+
err = new Error('userWatcherCallback error')
265+
throw err
266+
}
267+
},
268+
created () {
269+
child = this
270+
},
271+
render () {}
272+
}
273+
274+
new Vue({
275+
errorCaptured: spy,
276+
render: h => h(Child)
277+
}).$mount()
278+
279+
child.foo = 'bar'
280+
281+
waitForUpdate(() => {
282+
expect(spy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo"')
283+
expect(globalSpy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo"')
284+
}).then(done)
285+
})
286+
287+
it('should capture promise error from watcher', done => {
288+
const spy = jasmine.createSpy()
289+
290+
let child
291+
let err
292+
const Child = {
293+
data () {
294+
return {
295+
foo: null
296+
}
297+
},
298+
watch: {
299+
foo () {
300+
err = new Error('userWatcherCallback error')
301+
return Promise.reject(err)
302+
}
303+
},
304+
created () {
305+
child = this
306+
},
307+
render () {}
308+
}
309+
310+
new Vue({
311+
errorCaptured: spy,
312+
render: h => h(Child)
313+
}).$mount()
314+
315+
child.foo = 'bar'
316+
317+
child.$nextTick(() => {
318+
waitForUpdate(() => {
319+
expect(spy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo" (Promise/async)')
320+
expect(globalSpy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo" (Promise/async)')
321+
}).then(done)
322+
})
323+
})
324+
325+
it('should capture error from immediate watcher', done => {
326+
const spy = jasmine.createSpy()
327+
328+
let child
329+
let err
330+
const Child = {
331+
data () {
332+
return {
333+
foo: 'foo'
334+
}
335+
},
336+
watch: {
337+
foo: {
338+
immediate: true,
339+
handler () {
340+
err = new Error('userImmediateWatcherCallback error')
341+
throw err
342+
}
343+
}
344+
},
345+
created () {
346+
child = this
347+
},
348+
render () {}
349+
}
350+
351+
new Vue({
352+
errorCaptured: spy,
353+
render: h => h(Child)
354+
}).$mount()
355+
356+
waitForUpdate(() => {
357+
expect(spy).toHaveBeenCalledWith(err, child, 'callback for immediate watcher "foo"')
358+
expect(globalSpy).toHaveBeenCalledWith(err, child, 'callback for immediate watcher "foo"')
359+
}).then(done)
360+
})
361+
362+
it('should capture promise error from immediate watcher', done => {
363+
const spy = jasmine.createSpy()
364+
365+
let child
366+
let err
367+
const Child = {
368+
data () {
369+
return {
370+
foo: 'foo'
371+
}
372+
},
373+
watch: {
374+
foo: {
375+
immediate: true,
376+
handler () {
377+
err = new Error('userImmediateWatcherCallback error')
378+
return Promise.reject(err)
379+
}
380+
}
381+
},
382+
created () {
383+
child = this
384+
},
385+
render () {}
386+
}
387+
388+
new Vue({
389+
errorCaptured: spy,
390+
render: h => h(Child)
391+
}).$mount()
392+
393+
waitForUpdate(() => {
394+
expect(spy).toHaveBeenCalledWith(err, child, 'callback for immediate watcher "foo" (Promise/async)')
395+
expect(globalSpy).toHaveBeenCalledWith(err, child, 'callback for immediate watcher "foo" (Promise/async)')
396+
}).then(done)
397+
})
250398
})

0 commit comments

Comments
 (0)
Please sign in to comment.