The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Create a dedicated worker | const worker = new Worker('/workers/tasks.js') | View examples |
| Create a module worker | new Worker('/workers/tasks.js', { type: 'module', name: 'tasks' }) | View examples |
| Send a typed message | worker.postMessage({ type: 'sum', id, values }) | View examples |
| Receive worker output | worker.addEventListener('message', event => use(event.data)) | View examples |
| Connect to a shared worker | const shared = new SharedWorker('/workers/shared.js', { name: 'counter' }) | View examples |
| Start a message port | shared.port.addEventListener('message', handler); shared.port.start() | View examples |
| Clone structured data | const copy = structuredClone({ map: new Map([['x', 1]]) }) | View examples |
| Transfer an ArrayBuffer | worker.postMessage({ buffer }, [buffer]) | View examples |
| Create a private channel | const { port1, port2 } = new MessageChannel() | View examples |
| Transfer a message port | worker.postMessage({ type: 'attach', port: port2 }, [port2]) | View examples |
| Close a port | port1.close() | View examples |
| Observe worker errors | worker.addEventListener('error', reportWorkerError) | View examples |
| Observe decode failures | worker.addEventListener('messageerror', reportBadMessage) | View examples |
| Terminate immediately | worker.terminate() | View examples |
Web workers run JavaScript in separate global scopes so CPU-heavy or independent work does not monopolize the page's event loop. They cannot manipulate the DOM, and communication is asynchronous: design explicit message protocols, validate payloads, transfer ownership of large buffers when appropriate, and define cancellation, failure, and shutdown behavior instead of treating a worker like an ordinary function call.
Step by step
Detailed examples
Give dedicated workers an explicit request protocol
A dedicated worker has its own event loop and global scope; document APIs such as window and the DOM are unavailable. Module workers support static imports, while importScripts is only for classic workers. Use a small set of versionable message types and correlation IDs, validate every payload, and reserve workers for enough work to justify startup and serialization costs.
// page.js
const worker = new Worker('/workers/sum.js', { type: 'module', name: 'sum-worker' });
let nextId = 0;
const pending = new Map();
worker.addEventListener('message', ({ data }) => {
if (data?.type !== 'result' || !pending.has(data.id)) return;
pending.get(data.id).resolve(data.total);
pending.delete(data.id);
});
function sumOffThread(values) {
const id = ++nextId;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
worker.postMessage({ type: 'sum', id, values });
});
}
// /workers/sum.js
self.addEventListener('message', ({ data }) => {
if (data?.type !== 'sum' || !Array.isArray(data.values)) return;
const total = data.values.reduce((sum, value) => sum + Number(value), 0);
self.postMessage({ type: 'result', id: data.id, total });
}); Choose cloning or ownership transfer deliberately
postMessage uses the structured clone algorithm, which supports many built-ins, typed arrays, and cyclic graphs but does not clone functions or DOM nodes; an unsupported value can throw DataCloneError while sending. A transfer list moves transferable resources such as ArrayBuffer and MessagePort instead of copying them. Transfer is irreversible: an ArrayBuffer is detached in the sender, so do it only after that side has finished using the bytes.
// page.js
const buffer = new Uint8Array([2, 4, 6, 8]).buffer;
worker.postMessage({ type: 'double', buffer }, [buffer]);
console.log(buffer.byteLength); // 0: ownership moved
worker.addEventListener('message', ({ data }) => {
console.log([...new Uint8Array(data.buffer)]); // [4, 8, 12, 16]
});
// worker.js
self.addEventListener('message', ({ data }) => {
if (data?.type !== 'double' || !(data.buffer instanceof ArrayBuffer)) return;
const values = new Uint8Array(data.buffer);
values.forEach((value, index) => { values[index] = value * 2; });
self.postMessage({ buffer: data.buffer }, [data.buffer]);
}); Route private conversations with MessageChannel
MessageChannel creates two entangled MessagePort endpoints. Transfer one endpoint to a worker, frame, or another port and retain the other to avoid multiplexing unrelated protocols over a global message handler. A transferred endpoint can no longer be used by its former owner. Start ports after listeners are installed and close both ends when the conversation is complete.
// page.js
const channel = new MessageChannel();
channel.port1.addEventListener('message', ({ data }) => {
console.log('worker reply:', data);
});
channel.port1.start();
worker.postMessage({ type: 'attach', port: channel.port2 }, [channel.port2]);
channel.port1.postMessage({ command: 'status' });
// worker.js
self.addEventListener('message', ({ data }) => {
if (data?.type !== 'attach' || !(data.port instanceof MessagePort)) return;
const port = data.port;
port.addEventListener('message', ({ data: request }) => {
if (request?.command === 'status') port.postMessage({ status: 'ready' });
});
port.start();
}); Plan cancellation, failure, and shutdown
Workers may fail while loading or executing, and message deserialization can fail separately. Install error and messageerror listeners, reject outstanding correlated requests, and apply timeouts where a lost response would hang the UI. terminate stops a dedicated worker immediately without cleanup; for cooperative shutdown, send an application message, abort asynchronous operations inside the worker, close resources, then call the worker global's close method. Synchronous CPU loops must periodically yield or split work because they cannot receive cancellation messages while blocking their own event loop.
// page.js
worker.addEventListener('error', event => {
console.error('Worker failed:', event.message);
});
worker.addEventListener('messageerror', event => {
console.error('Worker could not decode a message', event);
});
function stopWorker() {
worker.postMessage({ type: 'shutdown' });
}
// worker.js
const jobs = new Set();
self.addEventListener('message', async ({ data }) => {
if (data?.type === 'shutdown') {
jobs.forEach(controller => controller.abort());
jobs.clear();
self.close();
return;
}
if (data?.type !== 'fetch') return;
const controller = new AbortController();
jobs.add(controller);
try {
const response = await fetch(data.url, { signal: controller.signal });
self.postMessage({ type: 'fetched', id: data.id, ok: response.ok });
} catch (error) {
if (error.name !== 'AbortError') throw error;
} finally {
jobs.delete(controller);
}
}); Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



