Web Workers
JavaScript
Promise
Asynchronous Programming
API Integration

How to write a Promise wrapper around Web Workers API?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Web Workers are already asynchronous, but the raw event-based API is awkward when you want a clean request-result flow. A Promise wrapper lets you treat a one-off worker job like any other async function: send input, await output, and handle errors in one place.

The Core Worker Contract

A worker communicates with the main thread through messages. The main thread posts data with postMessage, and the worker replies with its own postMessage.

A minimal worker file might look like this:

javascript
1// worker.js
2self.onmessage = (event) => {
3  const numbers = event.data;
4  const result = numbers.reduce((sum, value) => sum + value, 0);
5  self.postMessage(result);
6};

Without a wrapper, the caller has to manage onmessage, onerror, and cleanup manually every time.

A Simple Promise Wrapper

For one-shot jobs, wrap the worker in a Promise and terminate it when the result arrives.

javascript
1function runWorker(workerUrl, payload) {
2  return new Promise((resolve, reject) => {
3    const worker = new Worker(workerUrl, { type: "module" });
4
5    worker.onmessage = (event) => {
6      resolve(event.data);
7      worker.terminate();
8    };
9
10    worker.onerror = (error) => {
11      reject(error);
12      worker.terminate();
13    };
14
15    worker.postMessage(payload);
16  });
17}
18
19runWorker(new URL("./worker.js", import.meta.url), [1, 2, 3, 4])
20  .then((result) => console.log(result))
21  .catch((error) => console.error(error));

This gives you a normal Promise-based interface while still using the worker thread underneath.

Why This Pattern Works Well

A Promise wrapper is a good fit when each worker instance does exactly one job. It simplifies these concerns:

  • success handling
  • failure handling
  • cleanup with terminate()
  • 'await integration in calling code'

The caller no longer needs to remember worker lifecycle details for every use.

Add Structured Error Handling

Worker runtime errors and application-level errors are different. If your worker code can fail in a normal way, send a structured message instead of relying only on onerror.

javascript
1// worker.js
2self.onmessage = (event) => {
3  try {
4    const numbers = event.data;
5
6    if (!Array.isArray(numbers)) {
7      throw new Error("Expected an array of numbers");
8    }
9
10    const result = numbers.reduce((sum, value) => sum + value, 0);
11    self.postMessage({ ok: true, result });
12  } catch (error) {
13    self.postMessage({ ok: false, message: error.message });
14  }
15};

Then unwrap it in the Promise wrapper:

javascript
1function runWorker(workerUrl, payload) {
2  return new Promise((resolve, reject) => {
3    const worker = new Worker(workerUrl, { type: "module" });
4
5    worker.onmessage = (event) => {
6      const message = event.data;
7      worker.terminate();
8
9      if (message.ok) {
10        resolve(message.result);
11      } else {
12        reject(new Error(message.message));
13      }
14    };
15
16    worker.onerror = (error) => {
17      worker.terminate();
18      reject(error);
19    };
20
21    worker.postMessage(payload);
22  });
23}

That separates JavaScript execution errors from your own domain-level validation failures.

When One Worker Per Call Is Not Enough

If you need to reuse a worker for many requests, a single Promise wrapper is not enough by itself. You then need request IDs so multiple outstanding messages can be matched to the correct Promise.

That is a more advanced pattern, but the same idea holds: store a resolver and rejector for each request, and clear them when the matching response arrives.

Common Pitfalls

A common mistake is forgetting to terminate a one-shot worker. That leaves background threads alive longer than necessary.

Another mistake is assuming onerror catches every logical failure. It only catches worker script errors, not all application-level bad input. Send structured error messages when the worker can reject validly.

A third issue is posting data that cannot be cloned efficiently. Large payloads can make worker communication slower than expected, so consider transferables when moving big buffers.

Summary

  • A Promise wrapper makes one-off worker usage much cleaner
  • Wrap onmessage and onerror inside a Promise and terminate after completion
  • Use structured messages if the worker needs to report application-level failures
  • Reused workers require request IDs, not just one Promise for the whole worker
  • Watch payload size because worker messaging still has transfer costs

Course illustration
Course illustration

All Rights Reserved.