CPU-heavy JavaScript can occupy the Node.js event loop long enough to delay requests, timers, and other callbacks. A worker thread moves that calculation into a separate JavaScript thread while the main thread coordinates its input, result, and failure state.
The built-in node:worker_threads module can load the same ES module in both contexts. isMainThread selects the parent or worker branch, workerData carries cloneable input into the worker, and parentPort.postMessage() sends the calculated value back.
One new Worker has startup overhead, so this pattern fits an occasional CPU task better than a stream of tiny jobs. Repeated CPU work usually belongs in a worker pool, while a single worker is sufficient when the main thread needs one prime count and the worker's thread ID.
Steps to run a Node.js task in a worker thread:
- Create prime-worker.mjs with the worker-thread imports and primality helper.
- prime-worker.mjs
import { Worker, isMainThread, parentPort, threadId, workerData, } from 'node:worker_threads'; function isPrime(value) { if (value < 2) { return false; } for (let factor = 2; factor * factor <= value; factor += 1) { if (value % factor === 0) { return false; } } return true; }
- Append the prime-counting function beneath isPrime().
- prime-worker.mjs
function countPrimes(limit) { let count = 0; for (let candidate = 2; candidate <= limit; candidate += 1) { if (isPrime(candidate)) { count += 1; } } return count; }
- Append the worker branch beneath countPrimes() to handle one CPU task.
- prime-worker.mjs
if (!isMainThread) { const { limit } = workerData; if (!Number.isInteger(limit) || limit < 2) { throw new RangeError('limit must be an integer of at least 2'); } parentPort.postMessage({ threadId, count: countPrimes(limit), }); }
An uncaught worker exception emits the parent worker's error event, while postMessage() delivers the successful result through its message event.
- Append the main-thread wrapper beneath the worker branch to manage the worker lifecycle.
- prime-worker.mjs
function runTask(limit) { return new Promise((resolve, reject) => { let receivedMessage = false; const worker = new Worker(new URL(import.meta.url), { workerData: { limit }, }); worker.once('message', (message) => { receivedMessage = true; resolve(message); }); worker.once('error', reject); worker.once('exit', (code) => { if (code !== 0) { reject(new Error(`Worker stopped with exit code ${code}`)); } else if (!receivedMessage) { reject(new Error('Worker exited without returning a result')); } }); }); }
new URL(import.meta.url) reloads this module as the worker program, and the isMainThread conditions prevent the parent entry point from running inside that worker.
- Append the command-line entry point beneath runTask().
- prime-worker.mjs
if (isMainThread) { const limit = Number(process.argv[2] ?? '200000'); try { const result = await runTask(limit); console.log( `Worker ${result.threadId} counted ${result.count} primes up to ${limit}.`, ); } catch (error) { console.error(`Worker task failed: ${error.message}`); process.exitCode = 1; } }
- Run the worker with an invalid limit to confirm failures reach the main thread.
$ node prime-worker.mjs 1 Worker task failed: limit must be an integer of at least 2
The error path sets a nonzero process exit code, so a shell script or service manager can distinguish the failed task from a successful result.
- Run the worker with a CPU-bound limit to receive the computed result.
$ node prime-worker.mjs 200000 Worker 1 counted 17984 primes up to 200000.
The nonzero thread ID identifies the worker execution context, while the prime count is the message returned to the main thread.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.