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.
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; }
function countPrimes(limit) { let count = 0; for (let candidate = 2; candidate <= limit; candidate += 1) { if (isPrime(candidate)) { count += 1; } } return count; }
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.
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.
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; } }
$ 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.
$ 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.