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

  1. Check status first: if (m.status === 'unlinked') await m.link(linker).
  2. Cache linked modules and reuse them — link once, then evaluate/use as needed.
  3. 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

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


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/8aed6344b453f44e. Report an issue: GitHub.