Vue version
3.5.41 (also reproduces on 3.5.40 and 3.6.0-rc.3)
Link to minimal reproduction
https://play.vuejs.org/#eNqNVs1u4zYQfpWBL6RQ2c42uz0YtoFsemhRtLtIc1vtgZHGFhtqSJCUEiMQ0IfoE/ZJCpKyJTsbbC8SNDOc75tf6mV2Y8yia3G2mq1daaXx4NC3ZluQbIy2Hl6gwp0kvNWN0YTkc6hzcLVQSj/d4S4HGx6aftcteaxy2KO/ba1F8r+S84JKzOHP1hkkhzn8hmhulOwQethZ3QDrWmQFFVRqch4qVOIAG+CNy2CzBcIn+Gx1Ix1ybqPIob+XDerWc5tD47JsPK70HjaBEv/yNTsK0Vpt3Wu5qKqIFL0qvV90QrW4MK2reROdLpeRsANfI9x9+nQP62MoWzBIlaQ97LSF66urxsG/f/8DkuCP9tmDr6UD6UAYM1fYoYrehDtQmXIMy+jVeanU3LZEwVd9qKzwUhPoXVRLkl4KBUbscXEkfqe1/xkNbC6Lw18KAiDR4ArYYMXyIJsA8wyiGYB4EnJIOb++usqS1KJvLQGPaak5q2THcmBW62BrwKKoDiwa93lB/ST9jXjEz2KPIa2BRXAR7L5JMxHNByrf4nfO8N2R4NsUkSq0WMVsrYDBDxFiONZHqCndQPUGNifanEUJy6YGH18ZfGQTHyaFO04Ejz5OLh7xcK5mgh176z7W37alby2mtkkhuFXUw7TdeChABltYn2ZoG74mFgZt5BOsIq/ymPFT57REaL/XONEo9c1FRaaJPxaj5uN4U6tUPpYvVl60yq/Oj6RjQ9m+TKVBPjRull8qToHn8AKNeF7BB+jP4L4H+ibjizNQ8zhxcSME9SMeVuGRJNBn0F/yuxB8HT/7i2F5vSJ5thDG3Gry+OwXpaad3C/i3vpFUKViyTjatADTPptuK7R20aBzofZja5LoYDNMFlfiAVUeOyKHx+gn5m0MEzZRG4RjoBt4jOtjMoc/pTkUVZWcZgX1AfR0B/ABcgQ5XzTZKCLRcfZuEd5yLzyCgPkWHuCplgrjAoxrZ9Lk0qWVeVy/LE9TmgN7YNkl2PurkSz7cfHKm0WnVYdVXN3j3VSKssYq4hN2aOdNigw60hWyywCuJwE8hADEwOomhzTuZ+bvX8cb4Gvp00VjtHSasEo0AMnbw1thRocf/g9+yEClKbIPbbhepht/G2q39tgYJTzGaVkPa2KZvmqb3sbi9uUl3pR/aUmcFQWxDPp+vQyqow10c7nbFLOhTRXS3tfFDJw/KNwUs1IrbVdQWtk4TcUs+Dyzhb4f9mCSc5etChqt3gaHbo7KYcIPPCWVqq3QnUKfbUmfOXfrZfjdWS8nGZj1/wEDZfRV
Steps to reproduce
The reproduction drives itself from onMounted — just open it and read the log. The tree is the one Nuxt renders for a page with <NuxtPage keepalive>:
<Suspense> <!-- root boundary, still pending -->
<RootDep /> <!-- async setup, resolves after 300ms -->
<KeepAlive>
<Suspense> <!-- per-page boundary -->
<component :is="page" :key="key" />
</Suspense>
</KeepAlive>
</Suspense>
- While
RootDep is still pending (so parentSuspense.deps > 0), switch the inner page from PageA to PageB.
- Let the root boundary resolve.
- Switch back to
PageA, then to PageB again.
What is expected?
The inner page follows the switches, and no errors are raised.
What is actually happening?
The rendered page is stuck on PageA forever — every later switch is a no-op — and the renderer throws:
Cannot read properties of null (reading 'suspenseId') // patchSuspense
Cannot read properties of null (reading 'm') // unmountComponent
In a production build the second one is the more familiar
Uncaught (in promise) TypeError: Cannot destructure property 'bum' of 'instance' as it is null.
Removing the <KeepAlive> and changing nothing else makes both errors go away and navigation work.
System Info
System:
OS: Linux 7.0 Ubuntu 26.04 LTS
CPU: (4) x64 Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
Memory: 5.86 GB / 11.67 GB
Binaries:
Node: 24.19.0
npm: 11.19.0
npmPackages:
vue: 3.5.41
Browsers:
Chromium (Playwright 1.5x, headless)
Any additional comments?
The chain, as far as I could trace it:
-
The route change re-renders KeepAlive, so the inner <Suspense> is patched with n1 != null. Because an ancestor boundary is still pending, SuspenseImpl.process takes its early return:
if (parentSuspense && parentSuspense.deps > 0 && !n1.suspense.isInFallback) {
n2.suspense = n1.suspense
n2.suspense.vnode = n2
n2.el = n1.el
return
}
The incoming page vnode is therefore never patched: vnode.component === null, vnode.el === null.
-
KeepAlive's onUpdated → cacheSubtree still caches it, unconditionally:
if (isSuspense(instance.subTree.type)) {
queuePostRenderEffect(() => {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}, instance.subTree.suspense)
}
-
The next render for that key hits the poisoned entry: KeepAlive copies cachedVNode.component (null) onto the new vnode and sets COMPONENT_KEPT_ALIVE, so patchSuspense reads newBranch.component.suspenseId on null, and the teardown that follows calls unmountComponent(null).
Suggested fix — the same shape as the two guards already at that site, i.e. do not cache a vnode that never finished mounting:
const innerChild = getInnerChild(instance.subTree)
if (innerChild.component) {
cache.set(pendingCacheKey, innerChild)
}
With that guard the page is simply not cached that one time, and the reproduction runs clean.
This looks like a third path into the defect class fixed by #10912 (avoid caching vnode that not been mounted, for #10899) and #11479 (for #6028) — both guard KeepAlive's own subtree, while here it is an ancestor Suspense that is pending.
Real-world context: this is how it surfaced for us — a Nuxt 4 app rendering <NuxtPage :keepalive="{ max: 5 }" />, where a link clicked before hydration finished intermittently left the user on a blank page with Cannot destructure property 'bum' of 'e' as it is null in the console. The window widens with slower hydration, so it shows up under load / on slow clients.
Investigation and reproduction prepared with Claude Code, on behalf of drunomics.
Vue version
3.5.41 (also reproduces on 3.5.40 and 3.6.0-rc.3)
Link to minimal reproduction
https://play.vuejs.org/#eNqNVs1u4zYQfpWBL6RQ2c42uz0YtoFsemhRtLtIc1vtgZHGFhtqSJCUEiMQ0IfoE/ZJCpKyJTsbbC8SNDOc75tf6mV2Y8yia3G2mq1daaXx4NC3ZluQbIy2Hl6gwp0kvNWN0YTkc6hzcLVQSj/d4S4HGx6aftcteaxy2KO/ba1F8r+S84JKzOHP1hkkhzn8hmhulOwQethZ3QDrWmQFFVRqch4qVOIAG+CNy2CzBcIn+Gx1Ix1ybqPIob+XDerWc5tD47JsPK70HjaBEv/yNTsK0Vpt3Wu5qKqIFL0qvV90QrW4MK2reROdLpeRsANfI9x9+nQP62MoWzBIlaQ97LSF66urxsG/f/8DkuCP9tmDr6UD6UAYM1fYoYrehDtQmXIMy+jVeanU3LZEwVd9qKzwUhPoXVRLkl4KBUbscXEkfqe1/xkNbC6Lw18KAiDR4ArYYMXyIJsA8wyiGYB4EnJIOb++usqS1KJvLQGPaak5q2THcmBW62BrwKKoDiwa93lB/ST9jXjEz2KPIa2BRXAR7L5JMxHNByrf4nfO8N2R4NsUkSq0WMVsrYDBDxFiONZHqCndQPUGNifanEUJy6YGH18ZfGQTHyaFO04Ejz5OLh7xcK5mgh176z7W37alby2mtkkhuFXUw7TdeChABltYn2ZoG74mFgZt5BOsIq/ymPFT57REaL/XONEo9c1FRaaJPxaj5uN4U6tUPpYvVl60yq/Oj6RjQ9m+TKVBPjRull8qToHn8AKNeF7BB+jP4L4H+ibjizNQ8zhxcSME9SMeVuGRJNBn0F/yuxB8HT/7i2F5vSJ5thDG3Gry+OwXpaad3C/i3vpFUKViyTjatADTPptuK7R20aBzofZja5LoYDNMFlfiAVUeOyKHx+gn5m0MEzZRG4RjoBt4jOtjMoc/pTkUVZWcZgX1AfR0B/ABcgQ5XzTZKCLRcfZuEd5yLzyCgPkWHuCplgrjAoxrZ9Lk0qWVeVy/LE9TmgN7YNkl2PurkSz7cfHKm0WnVYdVXN3j3VSKssYq4hN2aOdNigw60hWyywCuJwE8hADEwOomhzTuZ+bvX8cb4Gvp00VjtHSasEo0AMnbw1thRocf/g9+yEClKbIPbbhepht/G2q39tgYJTzGaVkPa2KZvmqb3sbi9uUl3pR/aUmcFQWxDPp+vQyqow10c7nbFLOhTRXS3tfFDJw/KNwUs1IrbVdQWtk4TcUs+Dyzhb4f9mCSc5etChqt3gaHbo7KYcIPPCWVqq3QnUKfbUmfOXfrZfjdWS8nGZj1/wEDZfRV
Steps to reproduce
The reproduction drives itself from
onMounted— just open it and read the log. The tree is the one Nuxt renders for a page with<NuxtPage keepalive>:RootDepis still pending (soparentSuspense.deps > 0), switch the inner page fromPageAtoPageB.PageA, then toPageBagain.What is expected?
The inner page follows the switches, and no errors are raised.
What is actually happening?
The rendered page is stuck on
PageAforever — every later switch is a no-op — and the renderer throws:In a production build the second one is the more familiar
Uncaught (in promise) TypeError: Cannot destructure property 'bum' of 'instance' as it is null.Removing the
<KeepAlive>and changing nothing else makes both errors go away and navigation work.System Info
System: OS: Linux 7.0 Ubuntu 26.04 LTS CPU: (4) x64 Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz Memory: 5.86 GB / 11.67 GB Binaries: Node: 24.19.0 npm: 11.19.0 npmPackages: vue: 3.5.41 Browsers: Chromium (Playwright 1.5x, headless)Any additional comments?
The chain, as far as I could trace it:
The route change re-renders
KeepAlive, so the inner<Suspense>is patched withn1 != null. Because an ancestor boundary is still pending,SuspenseImpl.processtakes its early return:The incoming page vnode is therefore never patched:
vnode.component === null,vnode.el === null.KeepAlive'sonUpdated→cacheSubtreestill caches it, unconditionally:The next render for that key hits the poisoned entry:
KeepAlivecopiescachedVNode.component(null) onto the new vnode and setsCOMPONENT_KEPT_ALIVE, sopatchSuspensereadsnewBranch.component.suspenseIdonnull, and the teardown that follows callsunmountComponent(null).Suggested fix — the same shape as the two guards already at that site, i.e. do not cache a vnode that never finished mounting:
With that guard the page is simply not cached that one time, and the reproduction runs clean.
This looks like a third path into the defect class fixed by #10912 (
avoid caching vnode that not been mounted, for #10899) and #11479 (for #6028) — both guardKeepAlive's own subtree, while here it is an ancestorSuspensethat is pending.Real-world context: this is how it surfaced for us — a Nuxt 4 app rendering
<NuxtPage :keepalive="{ max: 5 }" />, where a link clicked before hydration finished intermittently left the user on a blank page withCannot destructure property 'bum' of 'e' as it is nullin the console. The window widens with slower hydration, so it shows up under load / on slow clients.Investigation and reproduction prepared with Claude Code, on behalf of drunomics.