Worker Patterns
The request/response pattern
Section titled “The request/response pattern”Workers are asynchronous and may handle many tasks. To match a response to the right request, attach a unique id to each message:
let nextId = 0;const pending = {};worker.onmessage = (e) => { const { id, result } = e.data; if (pending[id]) { pending[id](result); delete pending[id]; }};function ask(payload) { return new Promise((resolve) => { const id = ++nextId; pending[id] = resolve; worker.postMessage({ id, payload }); });}// worker.jsself.onmessage = (e) => { const { id, payload } = e.data; self.postMessage({ id, result: process(payload) });};A tiny RPC wrapper
Section titled “A tiny RPC wrapper”Wrapping the pattern above into a small helper makes calling the worker feel like a regular async function call.
function createRpc(worker) { let seq = 0; const map = {}; worker.onmessage = (e) => { const cb = map[e.data.id]; if (cb) { cb(e.data.result); delete map[e.data.id]; } }; return { call(method, args) { return new Promise((res) => { const id = ++seq; map[id] = res; worker.postMessage({ id, method, args }); }); } };}Error handling with onerror
Section titled “Error handling with onerror”Uncaught errors inside a worker fire worker.onerror in the main thread — not window.onerror. Always attach a handler:
worker.onerror = (event) => { console.error('Worker error:', event.message, 'at', event.filename, ':', event.lineno); event.preventDefault(); // suppress browser console error};You can also attach self.onerror inside the worker itself to catch async errors before they bubble up to the main thread.
When to use a worker
Section titled “When to use a worker”| Use a worker | Do NOT use a worker |
|---|---|
| CPU-intensive loops (crypto, image processing, compression) | Simple DOM reads/writes |
| Parsing large JSON or binary data | Short tasks < 5 ms |
| Real-time data processing (audio, video) | Tasks needing direct DOM access |
| Keeping the UI at 60 fps during heavy work | One-off simple async fetches (use fetch directly) |
Runnable: request/response with message IDs
Section titled “Runnable: request/response with message IDs”Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| RPC wrapper (message ID + Promise) | Call the worker like a normal async function, and handle several concurrent requests systematically | More boilerplate to write (an id counter, a pending map) and an extra abstraction layer that’s slightly harder to debug |
Raw postMessage/onmessage | Simple, the flow is direct and visible — fine for a worker with a simple message pattern | With several requests in flight at once, you must match responses by hand, risking a race condition |
Attaching a worker.onerror handler | Catches uncaught errors, preventing a silent failure in production | Needs a handler on every worker, and you must design it so an error doesn’t leave a pending promise hanging forever |
Common mistakes
Section titled “Common mistakes”- Not attaching
worker.onerror— an uncaught error inside a worker never shows up inwindow.onerror. Without a handler, a crash fails silently and is very hard to detect in production. - Letting a
pending/map grow without ever removing entries — if a request never gets a response (e.g. the worker crashes), its entry sits in the map forever, becoming a memory leak. Add a timeout or a cleanup path for worker errors. - Using a worker for work that isn’t worth the overhead — like reading a button click or a single simple fetch. The cost of setting up an RPC wrapper and cross-thread communication is far higher than the benefit for such small tasks.