denoland/deno · error · Error
ERR_VM_MODULE_ALREADY_LINKED
ERR_VM_MODULE_ALREADY_LINKED
Error message
Module has already been linked
What it means
Module#link(linker) can run only once per module: it throws ERR_VM_MODULE_ALREADY_LINKED whenever status is anything other than 'unlinked' — including while a link is in flight ('linking') or after it completed. Re-linking a cached or shared module is the usual cause.
Source
Thrown at ext/node/polyfills/vm.js:625
if (this[kLinkingStatus] !== null) {
return this[kLinkingStatus];
}
return STATUS_NAMES[op_vm_module_get_status(this[kWrap])];
}
get error() {
if (this.status !== "errored") {
throw new ERR_VM_MODULE_STATUS("must be errored");
}
return op_vm_module_get_exception(this[kWrap]);
}
link(linker) {
if (typeof linker !== "function") {
throw new ERR_INVALID_ARG_TYPE("linker", "function", linker);
}
if (this.status !== "unlinked") {
throw new ERR_VM_MODULE_ALREADY_LINKED();
}
this[kLinkingStatus] = "linking";
return PromisePrototypeThen(this[kLink](linker), (v) => {
this[kLinkingStatus] = null;
return v;
}, (e) => {
this[kLinkingStatus] = null;
throw e;
});
}
// Two-phase linking so cyclic imports are supported. First walk the whole
// dependency graph, calling the linker for each module and recording its
// resolved dependencies via `op_vm_module_link`, WITHOUT instantiating.
// Then instantiate once at the root - V8 instantiates the entire graph in
// a single pass. Decoupling linking from instantiation is what lets a
// module that (transitively) imports itself resolve: the `visited` set
// short-circuits the cycle once a module has recorded its resolutions.View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Check status first: if (m.status === 'unlinked') await m.link(linker).
- Cache linked modules and reuse them — link once, then evaluate/use as needed.
- Serialize linking with a per-module memoized promise so concurrent callers await the same operation.
Example fix
// before
for (const run of runs) {
await m.link(linker); // second iteration throws ERR_VM_MODULE_ALREADY_LINKED
await m.evaluate();
}
// after
if (m.status === "unlinked") await m.link(linker);
await m.evaluate();
for (const run of runs) use(m.namespace); Defensive patterns
Strategy: validation
Validate before calling
const linkStates = new WeakMap();
async function linkOnce(m, linker) {
if (!linkStates.has(m)) {
linkStates.set(m, m.link(linker));
}
await linkStates.get(m);
} Try / catch
try {
await m.link(linker);
} catch (err) {
if (err.code === "ERR_VM_MODULE_ALREADY_LINKED") return; // idempotent
throw err;
} Prevention
- Route every link through one linkOnce helper.
- Never assume a fresh module per request unless you construct one; cached modules carry their link state.
- A link still in flight also blocks a second link — await the first promise, don't re-issue.
When it happens
Trigger: Calling m.link() twice; concurrent link() calls on the same module; a linker cache that returns an already-linked dependency which then gets linked again from a new entry point.
Common situations: Caching modules for repeated runs (hot reload, replays) but re-linking each run; parallel paths (Promise.all) both linking a shared dependency; retry wrappers that re-run link after an unrelated failure.
Related errors
- ERR_VM_MODULE_STATUS
- ERR_INVALID_ARG_TYPE
- ERR_VM_MODULE_NOT_MODULE
- ERR_VM_MODULE_DIFFERENT_CONTEXT
- ERR_MODULE_LINK_MISMATCH
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/8aed6344b453f44e.
Report an issue: GitHub.