MessageChannel

Creates a pair of linked ports forming a two-way communication channel between threads.

Signature
new MessageChannel()
  • Added in Node.js v10.5.0

Return value

<MessageChannel>An object with port1 and port2 properties, which refer to two linked MessagePort instances.

Errors

Nothing. The constructor takes no arguments and cannot fail under normal conditions.

new MessageChannel() yields { port1, port2 }: two MessagePort objects wired to each other. Anything posted on one arrives as a 'message' event on the other. The class itself has no methods; it exists purely to mint the pair. The API mirrors the browser's MessageChannel, so the same code shape works in both places.

When to reach for it

The built-in parentPort channel is a single lane between a worker and its creator. Reach for a MessageChannel when you want more:

  • Thread-to-thread links. Create the pair on the main thread, send port1 to worker A and port2 to worker B, and they talk directly without the main thread relaying every message.
  • Separate lanes. One channel for control messages, one for bulk data, so a large payload never delays a cancellation.
  • Per-request replies. Send a fresh port with each job and close it when the answer arrives, instead of correlating ids on one shared channel.

What trips people up

A port must be transferred, never cloned: pass it in the transferList argument of postMessage() (or the transferList option of new Worker()). Omit it and you get ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST. Once transferred, the port is unusable in the sending thread. An open port with a listener also keeps its thread alive, so close both ends when finished.

Example

Example · javascript
const { MessageChannel } = require('node:worker_threads');

const { port1, port2 } = new MessageChannel();

port1.on('message', (message) => {
  console.log('port1 received:', message);
  port1.close();
  port2.close();
});

port2.postMessage({ foo: 'bar' });

// Prints:
// port1 received: { foo: 'bar' }

When to use it

  • Wire two worker threads together so data does not relay through the main thread
  • Keep a separate control channel for cancellation alongside a busy data channel
  • Hand each request its own short-lived reply port instead of correlating ids
  • Give a worker a port into a shared logging or metrics thread

Frequently asked questions

How do two worker threads communicate without going through the main thread?

Create a MessageChannel on the main thread and transfer port1 to one worker and port2 to the other, then post directly between them. The ports must appear in the transferList of the postMessage call or the Worker constructor, otherwise the transfer throws.

Why do I get ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST?

Because a MessagePort was included in the message or in workerData without being listed in transferList. Ports cannot be structured-cloned — they can only be moved — so every port in a payload must also be named as transferable.

What is the difference between MessageChannel and BroadcastChannel?

A MessageChannel connects exactly two endpoints and its ports can be transferred anywhere, while BroadcastChannel delivers each message to every thread that has joined the same named channel. Use MessageChannel for point-to-point work distribution and BroadcastChannel for fan-out notifications such as shutdown signals.

More examples

Letting two workers talk to each other directly

The three words never pass through the main thread — it only creates the pair and hands one end to each worker. Both transferList entries are mandatory; dropping either one makes the Worker constructor throw ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST. The explicit null end marker exists because a channel has no built-in end-of-stream signal.

Example · javascript
// main.js
const { Worker, MessageChannel } = require('node:worker_threads');

const { port1, port2 } = new MessageChannel();

const producer = new Worker('./producer.js', {
  workerData: { port: port1 },
  transferList: [port1],   // required: ports are transferred, not cloned
});
const consumer = new Worker('./consumer.js', {
  workerData: { port: port2 },
  transferList: [port2],
});

consumer.on('message', (msg) => {
  console.log('main saw:', msg);
  Promise.all([producer.terminate(), consumer.terminate()]);
});

// producer.js
const { workerData } = require('node:worker_threads');
for (const word of ['alpha', 'beta', 'gamma']) {
  workerData.port.postMessage(word);
}
workerData.port.postMessage(null); // end marker

// consumer.js
const { parentPort, workerData } = require('node:worker_threads');
const seen = [];
workerData.port.on('message', (word) => {
  if (word === null) {
    workerData.port.close();
    parentPort.postMessage(`consumed ${seen.length}: ${seen.join(', ')}`);
    return;
  }
  seen.push(word);
});

// Prints:
// main saw: consumed 3: alpha, beta, gamma

A reply port per request

Each request carries its own private reply channel, so there is no need to tag messages with ids or to keep a pending-request map on either side. Because port1 uses once() and both ends are closed after the reply, no listener or port leaks between calls — the failure mode this avoids is a growing set of 'message' handlers on one shared channel.

Example · javascript
// main.js
import { Worker, MessageChannel } from 'node:worker_threads';

const worker = new Worker('./echo.js');

function ask(text) {
  const { port1, port2 } = new MessageChannel();
  return new Promise((resolve) => {
    port1.once('message', (reply) => {
      port1.close();
      resolve(reply);
    });
    worker.postMessage({ text, reply: port2 }, [port2]);
  });
}

console.log(await ask('one'));
console.log(await ask('two'));
await worker.terminate();

// echo.js
import { parentPort } from 'node:worker_threads';
parentPort.on('message', ({ text, reply }) => {
  reply.postMessage(text.toUpperCase());
  reply.close();
});

// Prints:
// ONE
// TWO

Discussion

  • Be the first to comment on this lesson.