Environment
Symptom
Once a few errors have been rendered by the dev error handler, every subsequent error render crashes with:
ERROR unreachable
at wasm://wasm/0002f63a:wasm-function[54]:0xb65c
at wasm://wasm/0002f63a:wasm-function[23]:0xae24
at BasicSourceMapConsumer._parseMappings (source-map/lib/source-map-consumer.js:322:44)
at _ErrorParser.sourceLoader (.nuxt/dev/index.mjs:4235:41)
at async #getSource (youch-core/build/index.js:142:22)
at async #enhanceFrames (youch-core/build/index.js:232:27)
From that point on the dev server never shows a real error again — only unreachable spam — until restart. The original errors are completely masked.
Cause
sourceLoader in runtime/internal/error/dev.ts creates a new SourceMapConsumer(rawSourceMap) per stack frame per error and never calls consumer.destroy():
const consumer = await new SourceMapConsumer(rawSourceMap);
const originalPosition = consumer.originalPositionFor({ line: frame.lineNumber!, column: frame.columnNumber! });
source-map@0.7.x parses mappings inside a shared wasm instance with its own heap; undestroyed consumers leak that heap (this is documented in the source-map README — consumers must be destroy()ed or used via SourceMapConsumer.with). When an allocation finally fails, the Rust code panics — that's the unreachable trap — and the shared wasm instance stays poisoned forever: every later _parseMappings call throws too.
With a real-world dev bundle map (.nuxt/dev/index.mjs.map, 6.3 MB, 634 sources) it takes ~38 consumers to hit the wall. A typical SSR error has 10+ "app" frames in .nuxt/dev/index.mjs, and each frame parses the same 6.3 MB map again, so 2–4 rendered errors are enough to kill the renderer for the rest of the session.
Minimal repro
import { readFile } from "node:fs/promises";
import { SourceMapConsumer } from "source-map"; // 0.7.6
const raw = await readFile(".nuxt/dev/index.mjs.map", "utf8"); // any multi-MB map
for (let i = 1; i <= 100; i++) {
const consumer = await new SourceMapConsumer(raw);
consumer.originalPositionFor({ line: 1, column: 0 });
// no destroy() — mirrors sourceLoader
}
Output on my machine:
consumer #38: CRASH -> unreachable
follow-up consumer: STILL CRASHING -> unreachable <- instance is poisoned for good
Suggested fix
Two independent layers:
- Cache one consumer (promise) per
fileName instead of one per frame — bounded wasm heap, and frames of the same file stop re-parsing the same map. (Or destroy() after use / SourceMapConsumer.with, at the cost of re-parsing per frame.)
- Wrap the sourcemap lookup in try/catch so that even if the consumer ever throws, the error renderer degrades to unmapped frames instead of replacing the user's real error with
unreachable.
Happy to send a PR if useful.
Environment
main:src/runtime/internal/error/dev.ts)Symptom
Once a few errors have been rendered by the dev error handler, every subsequent error render crashes with:
From that point on the dev server never shows a real error again — only
unreachablespam — until restart. The original errors are completely masked.Cause
sourceLoaderinruntime/internal/error/dev.tscreates anew SourceMapConsumer(rawSourceMap)per stack frame per error and never callsconsumer.destroy():source-map@0.7.xparses mappings inside a shared wasm instance with its own heap; undestroyed consumers leak that heap (this is documented in the source-map README — consumers must bedestroy()ed or used viaSourceMapConsumer.with). When an allocation finally fails, the Rust code panics — that's theunreachabletrap — and the shared wasm instance stays poisoned forever: every later_parseMappingscall throws too.With a real-world dev bundle map (
.nuxt/dev/index.mjs.map, 6.3 MB, 634 sources) it takes ~38 consumers to hit the wall. A typical SSR error has 10+ "app" frames in.nuxt/dev/index.mjs, and each frame parses the same 6.3 MB map again, so 2–4 rendered errors are enough to kill the renderer for the rest of the session.Minimal repro
Output on my machine:
Suggested fix
Two independent layers:
fileNameinstead of one per frame — bounded wasm heap, and frames of the same file stop re-parsing the same map. (Ordestroy()after use /SourceMapConsumer.with, at the cost of re-parsing per frame.)unreachable.Happy to send a PR if useful.