#15674·Vue.js

Reactivity (3.6): a throw while notifying one subscriber leaves the rest of the batch unnotified until the next unrelated write

Author: daopkCreated Sep 27, 2026Updated Sep 27, 2026

Vue version

3.6.0-rc.9 (also vue built from minor at 08cad76, via pkg.pr.new). 3.5.43 is not affected.

Link to minimal reproduction

https://play.vuejs.org/#eNptkN1qhDAUhF9lyE2UtbpQKFTUtu8RKDZ77Eo1CfmxK+K7Fzd120LvksyZb+ZkYS/G5FMgVrLKSdsbD0c+mEaofjTaeiyw1GWQejTB0ynDZ+vlGSs6q0fwKRAXSiiplfO4oN7GkwVzCd5yrOkuzahvkCRJUTe45FM7BMrnVKgrNZkzRGlZMyzohuDOJbiblYysnab9mex32DEVqipi+2YbqTyNZmg9NUIB1VvwXis8y6GXH7VgW8mgTtT1ik6CNY624l7/PFZF9Pzvv2YfDoI18tyqd0KrYh1L3V+naS7oHZZl+5j6VyqewG8Xjm1F8hzrWhVmt0ZmtMfzLlfFrwVZxl4nsq7XipXsPn/Ij3dW5o9s/QIau6Bt

Steps to reproduce

  1. Open the reproduction (Vue 3.6.0-rc.9, no Vapor).
  2. Click set x to undefined. The computed over x.value.y throws while the sync watcher checks it, and the error is shown.
  3. Click change another ref.

For comparison, the same file on 3.5.43: https://play.vuejs.org/#eNptkNFqhDAURH9lyEuUFS1s+yJq2/8IFBuvVapJMIldEf+9uKnbLfQtydw5MzcrezUmnT2xnBVWTr1xsOS8qYTqR6MnhxUTtQmkHo131CT4qp3ssKGd9Ag+e+JCCSW1sg4XlPt4tGLJwWuOLT6kBeUNEkUxygqXdK4HT+kSC3WlRkuCIK1bghXt4G2Xg9tFycA6aNp1NP2EPcRCFVloX+0jhaPRDLWjSiigePfOaYUXOfTysxRsL+lVQ22vqBGssrQXd/r3sciC53//Nft0EqySXa0+CLUKdSZq/zpNdUFvsa77x5R3qXgGv1049hXJcWxbkZnDGpjBHs6HXGR3C7KEvc002V4rlrNz+pQ+ntn2DZpknzo=

xml
<script setup>
import { ref, computed, watch } from 'vue'

const x = ref({ y: 'a' })
const y = computed(() => x.value.y)
watch(y, () => {}, { flush: 'sync' })

const other = ref(0)
</script>

<template>
  <button @click="x = undefined">set x to undefined</button>
  <button @click="other++">change another ref</button>
  <p>x is {{ x === undefined ? 'undefined' : 'set' }}</p>
  <p>other is {{ other }}</p>
</template>

What is expected?

After step 2 the page says "x is undefined", as it does on 3.5.43: the error in one subscriber is reported, and the other subscribers of x, here the component's render, still hear the change.

What is actually happening?

After step 2 the page keeps saying "x is set". It changes to "x is undefined" only at step 3, when other is written, which has nothing to do with x.

flush() in packages/reactivity/src/system.ts has no try/finally. When effect.notify() throws, the loop stops with notifyIndex and notifyBufferLength left as they were, so the rest of notifyBuffer is neither notified nor cleared, and the next endBatch() that finds notifyBufferLength non-zero delivers it:

typescript
export function flush(): void {
  while (notifyIndex < notifyBufferLength) {
    const effect = notifyBuffer[notifyIndex]!
    notifyBuffer[notifyIndex++] = undefined
    effect.notify()
  }
  notifyIndex = 0
  notifyBufferLength = 0
}

System Info

bash
System:
  OS: macOS 27.0
  CPU: (12) arm64 Apple M2 Pro
  Shell: 5.9 - /bin/zsh
Binaries:
  Node: 24.21.0
  npm: 11.19.0
  pnpm: 12.6.0
Browsers:
  Chrome: 154.0.8037.57
  Safari: 27.0
npmPackages:
  vue: 3.6.0-rc.9

Any additional comments?

  • The same happens with plain reactivity and no component, whatever throws first: a computed read by a sync watch or by effect(), an effect() whose function throws, or a sync watch whose getter or callback throws (outside an app, where those rethrow). Every subscriber of the same ref that sits after it in the batch, such as a later sync watch or a later effect(), runs only at the next unrelated write on 3.6.0-rc.9 and minor. On 3.5.43 they all run at once, and the first error is rethrown afterwards.
  • Inside an app, an error from a watcher's getter or callback goes to app.config.errorHandler and stalls nothing. A computed that throws while a sync watcher checks whether it is dirty does not go there: that check runs in notify(), outside the watcher's call(...), so the error leaves flush() and comes out of the assignment. The escape itself also happens on 3.5.43; the stall does not.
  • alien-signals had the same bug and fixed it in stackblitz/alien-signals#97 (bfd4be520, "ensure effect queue is properly cleaned up after flush error") by putting its flush() in a try/finally that re-arms the effects it did not reach and resets the counters. #14813 synced system.ts to alien-signals v3.2.0, which includes that fix, but Vue's own flush() was left as it was. Porting that fix would stop the late delivery at an unrelated write; notifying the rest of the batch and then rethrowing, as 3.5 does, is what would also keep the page above correct.
  • Found through #15673, where a Vapor prop getter throws in that dirty check.