parentPort
Gives a worker the port it uses to send messages back to the thread that spawned it.
worker_threads.parentPort- Added in Node.js v10.5.0
Return value
<null> | <MessagePort>A MessagePort connected to the parent thread when this code runs inside a Worker; null on the main thread.
Errors
Nothing on read, but it is null on the main thread, so calling parentPort.postMessage() there throws a TypeError for reading a property of null.
Inside a Worker, parentPort is the worker's end of the built-in channel to whoever created it. It is a MessagePort, so it has postMessage(), on('message'), close(), ref() and unref(). On the main thread it is null.
When to reach for it
Every time a worker needs to return anything. parentPort.postMessage(result) is the counterpart of worker.on('message') in the parent, and parentPort.on('message', handler) receives what the parent sent with worker.postMessage(). For a long-lived pool thread, register the handler once at the top of the script and let it answer requests forever.
What trips people up
- Values are structured-cloned, not shared. Functions, class identity, promises and closures do not survive; a class instance arrives as a plain object. Send data, not behaviour.
- Attaching a
'message'listener keeps the thread's event loop alive, which is usually what you want for a pool but means a one-shot worker never exits. CallparentPort.unref()orclose()when the work is done. - A value that cannot be cloned raises a
DataCloneErrorat the call site in the worker, not in the parent. - Guard any shared module with
isMainThreadrather than assumingparentPortexists.
Example
const { Worker, isMainThread, parentPort } = require('node:worker_threads');
if (isMainThread) {
console.log('parentPort on main thread:', parentPort);
const worker = new Worker(__filename);
worker.on('message', (msg) => {
console.log('parent received:', msg);
worker.terminate();
});
worker.postMessage({ text: 'hello threads' });
} else {
parentPort.on('message', ({ text }) => {
parentPort.postMessage({ upper: text.toUpperCase(), length: text.length });
});
}
// Prints:
// parentPort on main thread: null
// parent received: { upper: 'HELLO THREADS', length: 13 }When to use it
- Return a computed hash, parsed document or resized image to the caller
- Keep a pool thread alive answering jobs as they arrive
- Report progress percentages from a long-running computation
- Send structured error objects back instead of throwing inside the thread
Frequently asked questions
Why is parentPort null in my script?
Because the script is running on the main thread, where there is no parent to talk to — parentPort is only a MessagePort inside a Worker. If a file is used both as an entry point and as a worker body, branch on isMainThread instead of assuming parentPort exists.
How does a worker send a result back to the main thread?
Call parentPort.postMessage(value) inside the worker; the parent receives it on worker.on('message'). The value is copied with the structured clone algorithm, so functions and class prototypes are lost and large buffers should be moved with a transferList rather than copied.
Why does my worker never exit after posting its result?
An active parentPort.on('message') listener keeps the worker's event loop alive indefinitely. Call parentPort.close() or parentPort.unref() when the work is finished, or terminate the thread from the parent.
More examples
A request/response worker with error handling
Each message carries an id so the parent can match replies to requests — the channel gives no ordering guarantee you should rely on once work becomes asynchronous. Failures are reported as ordinary messages rather than thrown, which keeps the thread alive for the next request; a real throw would end the thread and surface on the parent's 'error' event instead.
// sum-worker.js
const { parentPort } = require('node:worker_threads');
parentPort.on('message', ({ id, numbers }) => {
try {
if (!Array.isArray(numbers)) throw new TypeError('numbers must be an array');
const total = numbers.reduce((a, b) => a + b, 0);
parentPort.postMessage({ id, ok: true, total });
} catch (err) {
// Errors must be sent as data; a throw here would fire the parent's
// 'error' event and kill the thread.
parentPort.postMessage({ id, ok: false, error: err.message });
}
});
// main.js
const { Worker } = require('node:worker_threads');
const worker = new Worker('./sum-worker.js');
worker.on('message', (res) => {
console.log(res.id, res.ok ? res.total : `failed: ${res.error}`);
if (res.id === 2) worker.terminate();
});
worker.postMessage({ id: 1, numbers: [1, 2, 3] });
worker.postMessage({ id: 2, numbers: 'oops' });
// Prints:
// 1 6
// 2 failed: numbers must be an arrayA one-shot worker that exits cleanly
The worker does its job at module top level and posts once. Because it never registers a listener on parentPort, nothing holds its event loop open and it exits with code 0 on its own. The commented close() is the fix for the opposite case: a worker that listens for work and then appears to hang after the last reply.
// digest-worker.js
const { parentPort, workerData } = require('node:worker_threads');
const { createHash } = require('node:crypto');
const digest = createHash('sha256').update(workerData.input).digest('hex');
parentPort.postMessage(digest);
// Nothing is listening, so the thread's event loop drains and it exits.
// If a 'message' listener had been attached, this call would be required:
// parentPort.close();
// main.js
const { Worker } = require('node:worker_threads');
const worker = new Worker('./digest-worker.js', { workerData: { input: 'abc' } });
worker.on('message', (d) => console.log('sha256:', d.slice(0, 16)));
worker.on('exit', (code) => console.log('worker exited with', code));
// Prints:
// sha256: ba7816bf8f01cfea
// worker exited with 0
Discussion