Jest’s worker threads are a powerful tool for running tests in parallel, but they can be tricky to set up correctly.

// worker.js
const { parentPort, workerData } = require('worker_threads');

async function performTask() {
  const { input } = workerData;
  // Simulate some heavy computation
  await new Promise(resolve => setTimeout(resolve, 100));
  const result = input * 2;
  parentPort.postMessage({ result });
}

performTask();
// my.test.js
const { Worker } = require('worker_threads');
const path = require('path');

describe('Worker Threads', () => {
  it('should execute a task in a worker thread', async () => {
    const workerPath = path.resolve(__dirname, 'worker.js');
    const worker = new Worker(workerPath, {
      workerData: { input: 5 }
    });

    const result = await new Promise((resolve, reject) => {
      worker.on('message', resolve);
      worker.on('error', reject);
      worker.on('exit', (code) => {
        if (code !== 0) {
          reject(new Error(`Worker stopped with exit code ${code}`));
        }
      });
    });

    expect(result.result).toBe(10);
  });
});

This setup demonstrates how to spawn a worker thread from a Jest test. The worker_threads module allows Node.js to create separate threads of execution, which can be used to offload CPU-intensive tasks from the main event loop, preventing tests from blocking each other and speeding up overall test execution. In the example, worker.js performs a simple multiplication, and my.test.js launches this worker, passing data via workerData and receiving the result through parentPort.postMessage.

The core problem worker threads solve in testing is the Global Interpreter Lock (GIL) in some environments, or simply the single-threaded nature of the Node.js event loop. While Node.js is asynchronous, it’s still fundamentally single-threaded for JavaScript execution. Long-running synchronous operations or even complex asynchronous operations can still monopolize the event loop, making it unresponsive. Worker threads provide true parallelism by running JavaScript code in separate OS threads, allowing multiple tests or parts of a test to execute concurrently without blocking the main thread. This is particularly beneficial for tests that involve heavy computation, I/O-bound operations that can be batched, or simulations that would otherwise bog down the event loop.

When you use worker_threads, you’re essentially creating a new, independent Node.js environment for each worker. This means they have their own V8 instance, their own event loop, and their own memory space. Communication between the main thread and workers happens via message passing, using parentPort.postMessage in the worker and worker.on('message', ...) in the main thread. This message passing is asynchronous and serializes data, so it’s not for high-frequency, low-latency communication, but it’s perfect for sending input data and receiving results. The workerData option is a convenient way to pass initial data to the worker when it’s created.

The worker.on('exit', ...) handler is crucial for detecting unexpected worker termination. An exit code of 0 signifies a clean exit. Any other code indicates an error or an unhandled exception within the worker thread, which your test should ideally catch and report. Similarly, worker.on('error', ...) will catch unhandled exceptions that occur within the worker thread.

The real magic of worker_threads in Jest is not just running a single worker, but orchestrating multiple workers to run tests in parallel. Jest itself uses worker threads under the hood to parallelize test execution across multiple CPU cores. When you configure Jest to use workers (which is often the default for larger projects), it partitions your test files and assigns them to different worker threads. Each worker thread then runs its own Jest instance, loading and executing tests independently. This dramatically reduces the total time it takes for your entire test suite to complete. The worker_threads module is the underlying Node.js mechanism that enables this parallelization.

What many don’t realize is that the workerData is cloned and passed by value. This means any objects or complex data structures you pass are copied into the worker’s memory space. Modifications made to these objects within the worker thread do not affect the original objects in the main thread, and vice-versa. This isolation prevents race conditions and makes reasoning about state easier, but it also means you can’t directly share mutable state between threads. If you need to share state, you’d look into SharedArrayBuffer or more complex IPC mechanisms, but for most test scenarios, passing data by value is sufficient and safer.

The next hurdle you’ll likely encounter is managing shared resources or state across multiple tests running in different worker threads, or dealing with specific module loading behaviors within workers.

Want structured learning?

Take the full Jest course →