The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Create a dedicated workerconst worker = new Worker('/workers/tasks.js')View examples
Create a module workernew Worker('/workers/tasks.js', { type: 'module', name: 'tasks' })View examples
Send a typed messageworker.postMessage({ type: 'sum', id, values })View examples
Receive worker outputworker.addEventListener('message', event => use(event.data))View examples
Connect to a shared workerconst shared = new SharedWorker('/workers/shared.js', { name: 'counter' })View examples
Start a message portshared.port.addEventListener('message', handler); shared.port.start()View examples
Clone structured dataconst copy = structuredClone({ map: new Map([['x', 1]]) })View examples
Transfer an ArrayBufferworker.postMessage({ buffer }, [buffer])View examples
Create a private channelconst { port1, port2 } = new MessageChannel()View examples
Transfer a message portworker.postMessage({ type: 'attach', port: port2 }, [port2])View examples
Close a portport1.close()View examples
Observe worker errorsworker.addEventListener('error', reportWorkerError)View examples
Observe decode failuresworker.addEventListener('messageerror', reportBadMessage)View examples
Terminate immediatelyworker.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

01

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.

Send correlated jobs to a module worker
// 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 });
});
Back to quick reference ↑
02

Share state through ports only where support fits

A SharedWorker can coordinate multiple compatible same-origin browsing contexts, but each connection arrives as a MessagePort rather than through the SharedWorker object itself. Availability and mobile support vary, so feature-detect it and keep a dedicated-worker, BroadcastChannel, server, or storage-based fallback. With addEventListener, call port.start; assigning onmessage starts delivery implicitly.

Maintain a counter shared by connected pages
// page.js
if ('SharedWorker' in globalThis) {
  const shared = new SharedWorker('/workers/counter.js', { name: 'counter' });
  shared.port.addEventListener('message', ({ data }) => {
    document.querySelector('output').value = data.value;
  });
  shared.port.start();
  document.querySelector('button').addEventListener('click', () => {
    shared.port.postMessage({ type: 'increment' });
  });
}

// /workers/counter.js
let value = 0;
self.addEventListener('connect', ({ ports: [port] }) => {
  port.addEventListener('message', ({ data }) => {
    if (data?.type === 'increment') port.postMessage({ value: ++value });
  });
  port.postMessage({ value });
  port.start();
});
Back to quick reference ↑
03

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.

Transfer a buffer to a worker and back
// 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]);
});
Back to quick reference ↑
04

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.

Attach a dedicated command channel
// 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();
});
Back to quick reference ↑
05

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.

Abort active work before cooperative shutdown
// 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);
  }
});
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. WHATWGHTML: Web workershtml.spec.whatwg.org
  2. WHATWGHTML: Safe passing of structured datahtml.spec.whatwg.org
  3. WHATWGHTML: Channel messaginghtml.spec.whatwg.org
  4. WHATWGHTML: Message portshtml.spec.whatwg.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback