promise-worker-transferable review
promise-worker-transferable 1.0.4 wraps Web Worker and Service Worker messages in promises and adds transfer lists to both directions. Our browser bundle measured 5.1 KB minified and 2 KB gzipped. The page side calls `postMessage()` and receives a correlated promise; the worker registers one handler and may return a value, promise, or `withTransferList()` result. This 2016 fork exists because upstream promise-worker declined transferable objects. It is a message bridge, not a worker pool, RPC schema, timeout system, or Node worker_threads library.
promise-worker-transferable 1.0.4 installed in 0.6 seconds and bundled to 5.1 KB minified in our sandbox, but it has no bundled types, timeout, or release after 2016. Keep it in a working legacy transfer path; choose Comlink or a worker-pool library for new code.
We installed it
| Install | ✓ · 0.6s | 4 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 2 KB | gzipped (5.1 KB minified), bundled with esbuild |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does promise-worker-transferable install cleanly?
Yes. In a fresh container with an empty cache, npm install promise-worker-transferable finished in 0.6s, leaving 4 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does promise-worker-transferable add to a browser bundle?
2 KB gzipped (5.1 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does promise-worker-transferable work with both ESM and CommonJS?
Yes. Both import 'promise-worker-transferable' and require('promise-worker-transferable') worked in Node 22 in our run. The package is published as CommonJS.
Does promise-worker-transferable include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
promise-worker-transferable or comlink: which should you use?
comlink: Use it for maintained worker RPC with method proxies, transfers, ESM, and TypeScript declarations. promise-worker-transferable 1.0.4 installed in 0.6 seconds and bundled to 5.1 KB minified in our sandbox, but it has no bundled types, timeout, or release after 2016.
When should you not use promise-worker-transferable?
New worker code needs current maintenance: all three releases are from September 2016 and the last repository push was in July 2020
Use it if
- A legacy browser worker already uses promise-worker and large ArrayBuffers must transfer instead of copy
- Request and response IDs should be hidden behind one promise-returning call
- One worker handler is enough and message routing by a `type` property is acceptable
- The same small bridge must cover Web Workers and a controlling Service Worker
- New worker code needs current maintenance: all three releases are from September 2016 and the last repository push was in July 2020
- Timeouts or cancellation are required: an unanswered request stays pending and remains in the internal callback map
- Error type, stack, or custom fields must cross the boundary: rejection carries only `error.message` and creates a new Error on the page
- TypeScript or native ESM is mandatory: our install found no declarations and the package has CommonJS metadata without an exports map
- Node worker threads or a task pool are the target: the README says the package is not designed for Node and it wraps one browser worker
Setup reality
Our fresh install of promise-worker-transferable 1.0.4 completed in 0.6 seconds and left 4 packages using 1 MB on disk. It has 2 direct dependencies, no peers, no TypeScript declarations, and 0 known audit vulnerabilities. The Apache-2.0 package is 68 KB unpacked.
Build two entry points. The page bundle imports promise-worker-transferable; the separate worker file imports promise-worker-transferable/register. CommonJS has no exports map, though both require() and ESM import worked through Node 22 interop in our check. esbuild produced 5.1 KB minified and 2 KB gzipped for the page import. No credentials, config file, native compiler, or service is involved.
A transferred ArrayBuffer is detached from its sender, leaving byteLength at 0. Transfer only data that side has finished using. The worker handler may return a plain value or promise. Wrap a response with withTransferList(value, list) to transfer its buffer back. Rejections preserve only the message string, and the worker logs failures with console.error; return an explicit result object when callers need codes or structured details.
There is no built-in deadline. If a worker dies or never replies, its promise and callback entry remain. terminate() forwards to the worker but does not reject in-flight calls, so application code must settle or replace its own pending operations. On the Service Worker controller path, pass [] when there is no payload transfer list; the implementation concatenates that value with its MessagePort, and an omitted value can produce an invalid list entry.
Patterns
Await one worker response send-worker-request
const PromiseWorker = require('promise-worker-transferable')
const worker = new Worker('worker.js')
const bridge = new PromiseWorker(worker)
const reply = await bridge.postMessage('ping')The page and worker are separate bundles. This entry belongs only in the page-side bundle.
Reply from the worker entry register-worker-handler
const register = require('promise-worker-transferable/register')
register((message) => {
if (message === 'ping') return 'pong'
return Promise.resolve(compute(message))
})One handler receives every request. Route multiple operations inside it with your own message field.
Move pixel bytes without copying transfer-buffer-to-worker
const imageData = ctx.getImageData(0, 0, width, height)
const result = await bridge.postMessage(
imageData, [imageData.data.buffer]
)
console.log(imageData.data.buffer.byteLength) // 0Transfer detaches the sender's buffer. Any page code that still needs those bytes must copy them first.
Return a worker-owned buffer transfer-buffer-from-worker
register((message, withTransferList) => {
const output = processPixels(message)
return withTransferList(output, [output.data.buffer])
})`withTransferList` is the second handler argument and can also be returned after asynchronous work.
Dispatch messages by type route-worker-operations
register((message, withTransferList) => {
switch (message.type) {
case 'resize': {
const out = resize(message)
return withTransferList(out, [out.buffer])
}
case 'stats': return computeStats(message)
default: throw new Error(`unknown type: ${message.type}`)
}
})A thrown error comes back as a rejected promise containing a fresh Error with only the original message.
Return structured failures yourself preserve-error-fields
register(async (message) => {
try {
return { ok: true, value: await work(message) }
} catch (error) {
return { ok: false, code: error.code, message: error.message }
}
})
const result = await bridge.postMessage(job)
if (!result.ok) handle(result.code)Normal rejection loses error names, codes, stacks, and custom properties across this bridge.
Stop awaiting a silent worker add-worker-timeout
function withTimeout(promise, ms) {
let timer
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('worker timeout')), ms)
})
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))
}
await withTimeout(bridge.postMessage(job), 10000)Racing stops the caller's wait but does not cancel worker work or remove the package's callback entry.
Track calls before termination terminate-worker
const pending = new Set()
function send(message, transfers) {
const task = bridge.postMessage(message, transfers)
pending.add(task)
return task.finally(() => pending.delete(task))
}
function shutdown() {
bridge.terminate()
pending.clear()
}`terminate()` is undocumented and leaves in-flight package promises unsettled. Your wrapper needs its own rejection policy.
Pass an explicit empty transfer list message-service-worker
const bridge = new PromiseWorker(navigator.serviceWorker)
const reply = await bridge.postMessage({ type: 'status' }, [])On the Service Worker path, `[]` avoids concatenating an omitted value beside the required MessagePort.
Wait until the page has a controller wait-for-service-worker
await navigator.serviceWorker.register('sw.js', { scope: './' })
if (!navigator.serviceWorker.controller) {
await new Promise((resolve) =>
navigator.serviceWorker.addEventListener('controllerchange', resolve, { once: true })
)
}
const bridge = new PromiseWorker(navigator.serviceWorker)Registration alone does not guarantee control. The Service Worker can call `clients.claim()` during activation to take over sooner.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| comlink | npm | Use it for maintained worker RPC with method proxies, transfers, ESM, and TypeScript declarations |
| threads | npm | Use it for typed worker APIs, observables, and pools in browser or Node projects |
| piscina | npm | Use it for a managed Node worker_threads pool with queuing and operational controls |
More web frontend guides
postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

