d2lang/d2 · error · Error
D2 instance has been disposed
Error message
D2 instance has been disposed
What it means
The D2 JS client throws "D2 instance has been disposed" from sendMessage when you call any API method (compile, render, encode, decode, version, jsVersion) after dispose() was invoked. dispose() sets this.disposed = true, terminates the underlying worker, and rejects all pending requests, so the instance can no longer forward messages. This first synchronous check at index.js:97 is a fast-fail guard before awaiting readiness.
Source
Thrown at d2js/js/src/index.js:97
const wasmExecContent = isNode ? await loadFile("./wasm_exec.js") : null;
const wasmBinary = await loadFile("./d2.wasm");
const messageHandler = this.setupMessageHandler();
this.worker.postMessage({
type: "init",
data: {
wasm: wasmBinary,
wasmExecContent: isNode ? wasmExecContent.toString() : null,
},
});
return messageHandler;
}
async sendMessage(type, data) {
if (this.disposed) {
throw new Error("D2 instance has been disposed");
}
await this.ready;
if (this.disposed) {
throw new Error("D2 instance has been disposed");
}
return new Promise((resolve, reject) => {
const id = this.nextRequestId++;
this.pendingRequests.set(id, { resolve, reject });
try {
this.worker.postMessage({ id, type, data });
} catch (error) {
this.pendingRequests.delete(id);
reject(error);
}
});
}
async compile(input, options = {}) {View on GitHub (pinned to 0d69dca6f5)
Solutions
- Create a new D2 instance after dispose() instead of reusing the old one
- Guard calls: track disposal yourself and skip the request if the instance is disposed
- Reorder logic so dispose() only runs after all awaited API calls finish
- Ensure the object you hold a reference to is the live instance (avoid caching disposed instances in a registry/pool)
Example fix
// before
const d2 = new D2();
await d2.dispose();
const svg = await d2.compile('x -> y'); // throws
// after
const d2 = new D2();
await d2.dispose();
const d2v2 = new D2();
const svg = await d2v2.compile('x -> y'); Defensive patterns
Strategy: try-catch
Validate before calling
if (d2.disposed) {
throw new Error('Cannot call D2: instance already disposed');
}
const svg = await d2.compile('a -> b'); Type guard
function isUsable(d2) {
return d2 instanceof D2 && !d2.disposed && d2.worker !== undefined;
} Try / catch
try {
const svg = await d2.compile(src);
} catch (err) {
if (err.message === 'D2 instance has been disposed') {
d2 = new D2(); // recreate and retry once
} else {
throw err;
}
} Prevention
- Only call dispose() in definitive teardown paths (page unload, server shutdown)
- Never reuse a D2 instance after dispose(); create a new one
- Track in-flight API calls and await them before disposing
- Avoid caching D2 instances in pools/registries without a disposed check
When it happens
Trigger: Calling d2.compile()/render()/encode()/decode()/version()/jsVersion() after d2.dispose() has been called on the same instance, including calls already queued in a promise chain that resolve after disposal.
Common situations: Component unmount / shutdown handlers disposing a shared D2 instance while async work is still issuing requests; double-use of a pool or cache returning a disposed instance; race between dispose() and an in-flight compile.
Related errors
- response.error.message
- response.error.message
- failed to close Playwright browser: %w
- failed to start new Playwright page: %w
AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31).
Data as JSON: /api/errors/2a2a993ed189ff89.
Report an issue: GitHub.