Fix: Web Worker Not Working — postMessage Ignored, Cannot Import Module, or Worker Crashes Silently
Quick Answer
How to fix Web Worker issues — postMessage data cloning, module workers, error handling, SharedArrayBuffer setup, Comlink, and common reasons workers silently fail.
The Problem
A Web Worker is created but messages are never received:
const worker = new Worker('worker.js');
worker.postMessage({ data: [1, 2, 3] });
worker.onmessage = (e) => {
console.log(e.data); // Never fires
};Or the worker fails to load with a module import:
// worker.js
import { heavyComputation } from './utils.js'; // SyntaxError: Cannot use import statementOr an error inside the worker crashes silently:
// worker.js
self.onmessage = (e) => {
const result = JSON.parse(e.data.value); // Throws — but no error in main thread
self.postMessage(result);
};Or postMessage with a non-clonable object fails:
worker.postMessage({ fn: () => 'hello' });
// DataCloneError: function is not transferableWhy This Happens
Web Workers have strict rules that differ from the main thread:
- Workers run in a separate global scope — no
window, no DOM access, nodocument. Onlyself(the worker global),fetch,IndexedDB,WebSockets, and Web APIs that are explicitly worker-safe. postMessageuses the structured clone algorithm — only serializable data crosses the worker boundary. Functions, class instances with methods, DOM nodes, andErrorobjects (partially) are rejected with aDataCloneError.- Worker errors don’t propagate to the main thread by default — you must set
worker.onerrorto catch crashes. An unhandled throw inside a worker kills it silently. - Module workers require explicit
{ type: 'module' }—new Worker('worker.js')loads as a classic script. To useimport, pass{ type: 'module' }as the second argument. - CORS rules apply to worker scripts — the worker script URL must be same-origin, or served with appropriate CORS headers. A URL from a CDN without CORS headers silently fails.
Fix 1: Set Up Event Listeners Before postMessage
Register onmessage before sending messages — and always handle errors:
// WRONG — listener registered after postMessage
const worker = new Worker('worker.js');
worker.postMessage('start');
worker.onmessage = (e) => console.log(e.data); // May miss the response
// CORRECT — register listeners first
const worker = new Worker('worker.js');
worker.onmessage = (e) => {
console.log('Received:', e.data);
};
worker.onerror = (e) => {
console.error('Worker error:', e.message, 'in', e.filename, 'line', e.lineno);
e.preventDefault(); // Prevent the error from propagating further
};
worker.onmessageerror = (e) => {
console.error('Message deserialization failed:', e);
};
worker.postMessage({ data: [1, 2, 3] });Worker side — always send responses and handle errors:
// worker.js
self.onmessage = (e) => {
try {
const result = processData(e.data);
self.postMessage({ success: true, result });
} catch (err) {
// Don't just throw — send the error back to main thread
self.postMessage({ success: false, error: err.message });
}
};
function processData(data) {
// Heavy computation here
return data.map(x => x * 2);
}Fix 2: Use Module Workers for ES Imports
Enable ES module syntax in workers with { type: 'module' }:
// WRONG — classic worker, no import support
const worker = new Worker('./worker.js');
// CORRECT — module worker
const worker = new Worker('./worker.js', { type: 'module' });// worker.js — now you can use import/export
import { heavyComputation } from './utils.js';
import { process } from 'some-npm-package'; // Works if bundled
self.onmessage = async (e) => {
const result = await heavyComputation(e.data);
self.postMessage(result);
};With Vite — use the ?worker suffix:
// Vite automatically handles worker bundling
import MyWorker from './worker.js?worker';
const worker = new MyWorker();
worker.postMessage('start');
// For inline workers (no separate file):
import MyWorker from './worker.js?worker&inline';With webpack — use Worker constructor inline:
// webpack 5 — worker URLs are resolved at build time
const worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});Create a worker from a string (no separate file needed):
const workerCode = `
self.onmessage = (e) => {
const result = e.data.map(x => x * x);
self.postMessage(result);
};
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));Fix 3: Transfer Ownership Instead of Cloning
For large data like ArrayBuffer, ImageBitmap, or OffscreenCanvas, transfer ownership instead of copying:
// SLOW — copies the entire buffer (structured clone)
const buffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB
worker.postMessage(buffer); // Clones: 100MB copied
// FAST — transfers ownership (zero-copy)
worker.postMessage(buffer, [buffer]);
// After transfer, 'buffer' is detached in the main thread — byteLength is 0
console.log(buffer.byteLength); // 0
// Transfer multiple objects
const imageBuffer = new ArrayBuffer(4 * 1024 * 1024);
const metaBuffer = new ArrayBuffer(1024);
worker.postMessage(
{ imageBuffer, metaBuffer },
[imageBuffer, metaBuffer] // Transfer list
);
// Worker side — receives the buffers, can transfer back
self.onmessage = (e) => {
const { imageBuffer } = e.data;
// Process imageBuffer...
const result = processImage(imageBuffer);
self.postMessage({ result }, [result]); // Transfer back
};Transferable types:
ArrayBufferMessagePortImageBitmapOffscreenCanvasReadableStream,WritableStream,TransformStream
Non-transferable (must be cloned or avoided):
- Functions
- DOM elements
- Class instances with prototype methods
- Symbols
WeakMap,WeakSet
Fix 4: Use Comlink for a Clean API
Comlink removes the need to manually manage postMessage/onmessage and makes worker calls look like regular async function calls:
// worker.js
import * as Comlink from 'https://unpkg.com/comlink/dist/esm/comlink.mjs';
const api = {
async processData(items) {
return items.map(x => x * 2);
},
async fetchAndProcess(url) {
const response = await fetch(url);
const data = await response.json();
return data.items.filter(item => item.active);
}
};
Comlink.expose(api);// main.js
import * as Comlink from 'https://unpkg.com/comlink/dist/esm/comlink.mjs';
const worker = new Worker('./worker.js', { type: 'module' });
const api = Comlink.wrap(worker);
// Call worker functions like async methods — no postMessage needed
const result = await api.processData([1, 2, 3, 4, 5]);
console.log(result); // [2, 4, 6, 8, 10]
const filtered = await api.fetchAndProcess('/api/data');Comlink with classes:
// worker.js
import * as Comlink from 'comlink';
class DataProcessor {
constructor(config) {
this.config = config;
this.cache = new Map();
}
process(data) {
const key = JSON.stringify(data);
if (this.cache.has(key)) return this.cache.get(key);
const result = heavyCompute(data, this.config);
this.cache.set(key, result);
return result;
}
}
Comlink.expose(DataProcessor);
// main.js
const RemoteProcessor = Comlink.wrap(new Worker('./worker.js', { type: 'module' }));
const processor = await new RemoteProcessor({ threads: 4 });
const result = await processor.process([1, 2, 3]);Fix 5: Share Memory with SharedArrayBuffer
For maximum performance, share memory directly between the main thread and workers:
// SharedArrayBuffer requires cross-origin isolation headers:
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp
// Check if SharedArrayBuffer is available
if (typeof SharedArrayBuffer === 'undefined') {
console.error('SharedArrayBuffer not available — check COOP/COEP headers');
}
// main.js — create shared buffer
const shared = new SharedArrayBuffer(4 * 1024); // 4KB shared memory
const view = new Int32Array(shared);
worker.postMessage({ shared }); // Send buffer (not cloned — it's shared)
// Write to shared memory
view[0] = 42;
// Atomically wait for worker response
Atomics.wait(view, 1, 0); // Wait until view[1] != 0
console.log('Worker result:', view[2]);
// worker.js
self.onmessage = (e) => {
const view = new Int32Array(e.data.shared);
// Read value set by main thread
const input = Atomics.load(view, 0);
// Compute result
const result = input * 2;
// Write result and signal completion
Atomics.store(view, 2, result);
Atomics.store(view, 1, 1); // Signal: done
Atomics.notify(view, 1, 1); // Wake up main thread
};Required HTTP headers for SharedArrayBuffer:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpFor Vite dev server:
// vite.config.js
export default {
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
}
}
}Fix 6: Worker Pool Pattern for CPU-Bound Tasks
For tasks that need multiple workers running in parallel:
class WorkerPool {
constructor(workerUrl, poolSize = navigator.hardwareConcurrency || 4) {
this.workers = Array.from({ length: poolSize }, () => {
const w = new Worker(workerUrl, { type: 'module' });
w.busy = false;
return w;
});
this.queue = [];
}
run(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const idle = this.workers.find(w => !w.busy);
if (idle) {
this._dispatch(idle, task);
} else {
this.queue.push(task);
}
});
}
_dispatch(worker, task) {
worker.busy = true;
worker.onmessage = (e) => {
worker.busy = false;
task.resolve(e.data);
if (this.queue.length > 0) {
this._dispatch(worker, this.queue.shift());
}
};
worker.onerror = (e) => {
worker.busy = false;
task.reject(new Error(e.message));
};
worker.postMessage(task.data);
}
terminate() {
this.workers.forEach(w => w.terminate());
}
}
// Usage
const pool = new WorkerPool('./compute-worker.js');
const results = await Promise.all(
largeDataset.map(chunk => pool.run(chunk))
);Still Not Working?
Worker script returns 404 — the worker URL must be accessible from the current origin. With bundlers, the worker file may end up at a different path than expected. Check the Network tab in DevTools to confirm the worker script is loading. With Vite, use ?worker imports; with webpack, use new Worker(new URL('./worker.js', import.meta.url)).
DataCloneError for class instances — only plain objects, arrays, and primitives are cloned. If you have a class with methods, serialize it: worker.postMessage(JSON.parse(JSON.stringify(myInstance))). For complex objects, use structuredClone() on the sending side to validate cloneability before sending.
Service Workers vs Web Workers — Service Workers intercept network requests and run independently of any page. If you accidentally created a Service Worker when you wanted a Web Worker, it won’t respond to onmessage from your page. Use new Worker(url) for computation tasks, navigator.serviceWorker.register(url) for network interception.
Worker messages out of order — postMessage guarantees delivery order from a single sender, but responses may arrive in a different order if the worker processes them asynchronously. Include a task ID in every message and match responses by ID:
let nextId = 0;
const pending = new Map();
function send(data) {
const id = nextId++;
worker.postMessage({ id, data });
return new Promise((resolve) => pending.set(id, resolve));
}
worker.onmessage = (e) => {
pending.get(e.data.id)?.(e.data.result);
pending.delete(e.data.id);
};For related JavaScript performance issues, see Fix: JavaScript Heap Out of Memory and Fix: Node.js Stream Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: WebAssembly (WASM) Not Working — Module Fails to Load, Memory Error, or JS Interop Broken
How to fix WebAssembly issues — instantiateStreaming vs instantiate, CORS for WASM files, linear memory limits, wasm-bindgen JS interop, imports/exports mismatch, and WASM in bundlers.
Fix: IndexedDB Not Working — Transaction Inactive, Upgrade Blocked, or Store Not Found
How to fix IndexedDB issues — transaction lifecycle, version upgrades, blocked events, cursor iteration, IDBKeyRange queries, and using idb wrapper library to avoid callback hell.
Fix: React useTransition Not Working — UI Still Freezes, isPending Never True, or Transition Not Deferred
How to fix React useTransition and startTransition issues — what counts as a transition, Suspense integration, concurrent rendering requirements, and common mistakes that prevent transitions from deferring.
Fix: React Warning — Each Child in a List Should Have a Unique key Prop
How to fix React's missing key prop warning — why keys matter for reconciliation, choosing stable keys, avoiding index as key pitfalls, keys in fragments, and performance impact.