denoland/deno · error · Error

ERR_CLOSED_MESSAGE_PORT

ERR_CLOSED_MESSAGE_PORT

Error message

Cannot send data on closed MessagePort

What it means

moveMessagePortToContext looks up the port's internal id; a null id means the port is detached — already close()d, or already moved (moving clears the id so the original becomes unusable). The check intentionally runs before the vm.Context check to give the clearer ERR_CLOSED_MESSAGE_PORT ('Cannot send data on closed MessagePort') error, mirroring Node's ordering.

Source

Thrown at ext/node/polyfills/worker_threads.ts:1753

// underlying port are deserialized without the global host-object
// deserializers, mirroring Node's behavior where the target context lacks
// the JS classes registered in the source realm: any host object (e.g. a
// crypto KeyObject) triggers `messageerror` with the
// `ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE` code, while plain transferable
// data is delivered as `message`.
function moveMessagePortToContext(
  port: MessagePort,
  context: object,
): object {
  if (!(ObjectPrototypeIsPrototypeOf(MessagePortPrototype, port))) {
    throw new ERR_INVALID_ARG_TYPE("port", "MessagePort", port);
  }
  // Node checks closed-port state before vm.Context to give a clearer
  // error when the port is detached -- order matters for tests in the
  // node_compat suite that pass an empty {} as the context.
  const portId = getMessagePortId(port);
  if (portId === null) {
    throw new ERR_CLOSED_MESSAGE_PORT();
  }
  const vm = lazyVm();
  if (!vm.isContext(context)) {
    throw new ERR_INVALID_ARG_TYPE("context", "vm.Context", context);
  }
  // Take ownership of the port: clear the id on the original so it can no
  // longer be used from this context.
  setMessagePortId(port, null);

  // Allocate the wrapper inside the target context so its prototype chain
  // is the target realm's (i.e., `wrapper instanceof Object` in the caller
  // realm is false, matching Node).
  const wrapper = vm.runInContext("({})", context);
  wrapper.onmessage = null;
  wrapper.onmessageerror = null;

  let enabled = false;
  let closed = false;

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Use a fresh port from a new MessageChannel() for each move; ports are single-use for this API.
  2. Order operations so close() only runs after the move completes; guard with a moved/closed flag.
  3. Catch ERR_CLOSED_MESSAGE_PORT and rebuild the channel rather than retrying the same port.

Example fix

// before
port.close();
moveMessagePortToContext(port, ctx); // ERR_CLOSED_MESSAGE_PORT

// after
import { MessageChannel, moveMessagePortToContext } from 'node:worker_threads';
const { port1 } = new MessageChannel();
moveMessagePortToContext(port1, ctx); // move a live port exactly once
Defensive patterns

Strategy: try-catch

Validate before calling

const movedOrClosed = new WeakSet<object>();
function closePort(p: MessagePort) { p.close(); movedOrClosed.add(p); }
function isUsable(p: MessagePort) { return !movedOrClosed.has(p); }

if (isUsable(port)) moveMessagePortToContext(port, ctx);

Try / catch

try {
  moveMessagePortToContext(port, ctx);
} catch (e) {
  if (e?.code === 'ERR_CLOSED_MESSAGE_PORT') {
    // rebuild the channel and retry with a fresh port
    const { port1 } = new MessageChannel();
    moveMessagePortToContext(port1, ctx);
  } else throw e;
}

Prevention

When it happens

Trigger: port.close() followed by moveMessagePortToContext(port, ctx); or calling moveMessagePortToContext twice with the same port — the second call sees the id cleared by the first.

Common situations: Retry logic reusing a port after a failed setup whose cleanup closed it; double registration during init; error handlers closing ports that a pending move still needs.

Related errors


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