From 5a8c01ed4e4119b83cdfe2e4b6c7b71c1f1e87aa Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sat, 27 Jun 2026 01:37:07 +0200 Subject: [PATCH] fix(reactivity): stop once watchers after callback throws --- packages/reactivity/__tests__/watch.spec.ts | 34 +++++++++++++++++++++ packages/reactivity/src/watch.ts | 8 +++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/reactivity/__tests__/watch.spec.ts b/packages/reactivity/__tests__/watch.spec.ts index db19070e03d..31f739830ff 100644 --- a/packages/reactivity/__tests__/watch.spec.ts +++ b/packages/reactivity/__tests__/watch.spec.ts @@ -157,6 +157,40 @@ describe('watch', () => { expect(onError.mock.calls[1]).toMatchObject(['oops in once watch']) }) + test('once option stops after sync callback throw', () => { + const onError = vi.fn() + const call: WatchOptions['call'] = function call(fn, type, args) { + if (Array.isArray(fn)) { + fn.forEach(f => call(f, type, args)) + return + } + try { + fn.apply(null, args) + } catch (e) { + onError(e, type) + } + } + + const source = ref(0) + const cb = vi.fn(() => { + throw 'oops in once watch' + }) + + watch(source, cb, { call, once: true }) + + source.value++ + expect(cb).toHaveBeenCalledTimes(1) + expect(onError.mock.calls).toMatchObject([ + ['oops in once watch', WatchErrorCodes.WATCH_CALLBACK], + ]) + + source.value++ + expect(cb).toHaveBeenCalledTimes(1) + expect(onError.mock.calls).toMatchObject([ + ['oops in once watch', WatchErrorCodes.WATCH_CALLBACK], + ]) + }) + test('watch with onWatcherCleanup', async () => { let dummy = 0 let source: Ref diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts index 2c537c59083..d1124e9d963 100644 --- a/packages/reactivity/src/watch.ts +++ b/packages/reactivity/src/watch.ts @@ -221,9 +221,11 @@ export function watch( if (once && cb) { const _cb = cb cb = (...args) => { - const res = _cb(...args) - watchHandle() - return res + try { + return _cb(...args) + } finally { + watchHandle() + } } }