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
bun install
bun run dev
- Open
http://localhost:3000 in a browser with devtools open
- 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:
- A
<link rel=\"stylesheet\"> tag in index.html pointing at a real CSS file (content doesn't matter — even an empty file triggers it).
- The entry module (
main.ts) calls both import.meta.hot.accept() and import.meta.hot.dispose(...). Either hook alone does not trigger.
- 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.
What version of Bun is running?
1.3.12What 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.tomldev.tsindex.htmlsrc/main.tssrc/App.sveltesrc/styles/tokens.css— empty file (/* */)Steps
bun installbun run devhttp://localhost:3000in a browser with devtools opensrc/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
.svelteedit: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:
<link rel=\"stylesheet\">tag inindex.htmlpointing at a real CSS file (content doesn't matter — even an empty file triggers it).main.ts) calls bothimport.meta.hot.accept()andimport.meta.hot.dispose(...). Either hook alone does not trigger..sveltefile. (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.sveltefiles, nested directory structure.Root cause (from reading
_bun/client/index-*.js)The HMR client maintains per-module state including an
onDisposearray:During an HMR cascade, modules with pending disposers get pushed to a work list
J:Then
Jis iterated to run disposers, and each module'sonDisposeis nulled immediately after: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 toJtwice; on the second iteration,K.onDisposehas already been nulled by the first iteration, and thefor...ofthrows.