Skip to content

Bun dev server: K.onDispose is not iterable during HMR when index.html links a stylesheet + entry has both accept() and dispose() #29418

@AaronReboot

Description

@AaronReboot

What version of Bun is running?

1.3.12

What platform is your computer?

macOS 25.4.0 (Darwin, arm64)

What steps can reproduce the bug?

Minimal repro — 5 files, ~30 LOC:

package.json

{
  "name": "bun-hmr-mini-repro",
  "private": true,
  "type": "module",
  "scripts": { "dev": "bun --hot dev.ts" },
  "devDependencies": {
    "bun-plugin-svelte": "0.0.6",
    "svelte": "^5.0.0"
  }
}

bunfig.toml

[serve.static]
plugins = ["bun-plugin-svelte"]

dev.ts

import index from './index.html';
Bun.serve({
  port: 3000,
  development: { hmr: true },
  routes: { '/*': index },
});

index.html

<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="./src/styles/tokens.css" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./src/main.ts"></script>
  </body>
</html>

src/main.ts

import { mount } from 'svelte';
import App from './App.svelte';

mount(App, { target: document.getElementById('root')! });

if (import.meta.hot) {
  import.meta.hot.accept();
  import.meta.hot.dispose(() => {});
}

src/App.svelte

<script lang=\"ts\">
  let n = \$state(1);
</script>

<span>{n}</span>

src/styles/tokens.css — empty file (/* */)

Steps

  1. bun install
  2. bun run dev
  3. Open http://localhost:3000 in a browser with devtools open
  4. Edit src/App.svelte — change \$state(1) to \$state(2)

What is the expected behavior?

HMR update applies cleanly. No console errors.

What do you see instead?

2–4 copies of this error appear in the browser console on every .svelte edit:

TypeError: K.onDispose is not iterable
    at X0 (http://localhost:3000/_bun/client/index-*.js:24:171)
    at globalThis.<computed> (http://localhost:3000/_bun/client/index-*.js:24:3607)
    at blob:http://localhost:3000/<uuid>:1:28

The HMR update does still apply visually — the app continues to work — so the bug is non-fatal. But the errors appear on every HMR cycle.

Ablation findings

I reduced a large app down to this minimum. The bug requires all three of the following; removing any one makes the error disappear:

  1. A <link rel=\"stylesheet\"> tag in index.html pointing at a real CSS file (content doesn't matter — even an empty file triggers it).
  2. The entry module (main.ts) calls both import.meta.hot.accept() and import.meta.hot.dispose(...). Either hook alone does not trigger.
  3. An HMR edit to a .svelte file. (TypeScript/JS edits were not tested here.)

Things that are not needed: multiple components, import diamonds, \$props(), {#if} blocks, \$effect, <style> blocks, CSS imports from inside .svelte files, nested directory structure.

Root cause (from reading _bun/client/index-*.js)

The HMR client maintains per-module state including an onDispose array:

class b {
  // ...
  onDispose = null;
  // ...
  dispose(Y) { (this.onDispose ??= []).push(Y) }
}

During an HMR cascade, modules with pending disposers get pushed to a work list J:

if (F.selfAccept) {
  if (G.add(F), Q.add(F), j = !1, F.onDispose) J.push(F)
} else if (Object.keys(F.data).length > 0) {
  if (F.selfAccept ??= t, G.add(F), Q.add(F), j = !1, F.onDispose) J.push(F)
}

Then J is iterated to run disposers, and each module's onDispose is nulled immediately after:

for (let K of J) {
  K.state = 1;
  for (let Q of K.onDispose) {   // <-- crashes here when K.onDispose === null
    let V = Q(K.data);
    if (V && V instanceof Promise) W.push(V);
  }
  K.onDispose = null;
}

The guard if (F.onDispose) J.push(F) only checks at push time. If the same module is reached via two paths in a single cascade, it gets pushed to J twice; on the second iteration, K.onDispose has already been nulled by the first iteration, and the for...of throws.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions