You’ve written this, probably more than once:
let resolve, reject;
const p = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
It’s the trick you reach for the moment you need to settle a Promise from outside its executor — a different event handler, a different module, a callback that fires whenever it feels like it. A Promise can only be resolved from inside the function you pass to new Promise. So you hoist resolve and reject out to let variables and fire them later. It works, but it’s a smuggling operation, and you re-derive it from scratch in every project.
That’s not a bug in Promises. It’s a boundary they were never meant to cross. A Promise is designed to wrap a single act of “start this, and eventually finish it” — not to be a signal you hold onto and fire when some external event decides to happen. But real code is full of exactly that shape: legacy callback APIs, one-shot event listeners, a WebSocket message you’re waiting for, a UI action that resolves a background job. So people reinvent the hoisted-resolve trick over and over, slightly worse each time.
Not a new idea — an old one that got left out
Deferred is what Promises looked like before Promise had a spec. jQuery shipped one — jQuery.Deferred, described as:
A factory function that returns a chainable utility object with methods to register multiple callbacks into callback queues, invoke callback queues, and relay the success or failure state of any synchronous or asynchronous function.
Q had its own. When native Promises landed, the language kept the executor model and dropped the externally-controllable object. For good reason, mostly: eagerly wrapping deferreds around things that are already promises is a genuine smell. Petka Antonov catalogued it in the Bluebird wiki as the Deferred anti-pattern:
In Deferred anti-pattern, “deferred” objects are created for no reason, complicating code.
He’s right — when the work already returns a promise, you should just return it. But there’s a real, irreducible case the executor model doesn’t serve: when nothing hands you a promise, and the thing that will eventually settle it lives somewhere else entirely. That’s the case Deferred is for.
So I packaged it. concurrency.libx.js exposes a Promise-compatible object where resolve and reject are public methods on the object itself, instead of trapped inside a closure you have to exfiltrate:
const { Deferred } = require('concurrency.libx.js');
const fs = require('fs');
const p = Deferred.new();
fs.stat('.', (err, value) => {
if (err) return p.reject(err);
p.resolve(value);
});
(async () => {
const stat = await p; // works exactly like awaiting a real Promise
})();
Same intent as util.promisify, but without requiring the callback to follow Node’s (err, result) convention and without needing to know which argument slot is the callback. Bridging a WebSocket onmessage to an await. A “wait for the user to click confirm” gate. Anywhere you’re currently juggling flags and event emitters to fake a one-shot signal — that’s a Deferred.
The language eventually agreed
Here’s the part that made me feel less like I’d built a crutch. In March 2024, Promise.withResolvers() shipped as Baseline — a Stage 4 TC39 proposal, now part of ECMAScript. MDN describes it as a method that:
returns an object containing a new
Promiseobject and two functions to resolve or reject it, corresponding to the two parameters passed to the executor of thePromise()constructor.
That is the hoisted-resolve trick, promoted into the standard library. The MDN page even opens its rationale with the exact let resolve, reject block from the top of this post. The idea wasn’t wrong — it was just missing.
Progress, and the rest of the plumbing
Later builds added progress reporting, because “waiting for one result” and “waiting for a stream of updates that eventually settles” are the same object shape, just with more calls before the final one:
const d = Deferred.new();
d.onProgress((progress) => console.log('Progress:', progress));
d.reportProgress(0.1);
d.reportProgress(0.5);
d.resolve('done');
It’s also an async iterator, so for await (const progress of d) drains every reported value and exits cleanly when the Deferred resolves — one object, two consumption styles, no second abstraction just because you also want intermediate updates.
The rest of the library is the stuff that keeps showing up next to Deferred, because it’s all the same underlying problem — controlling when and how often async work runs, not just whether it finishes: debounce / throttle (promise-aware, so you’re not racing a debounced call against one mid-flight), chain (strict sequence when Promise.all’s parallelism is wrong), async(fn) / isAsync(fn), and the boring waitUntil / delay / measurements utilities everyone rewrites, done once and tested.
Honest limits
If you only need to expose resolve/reject and you can target a modern runtime, use Promise.withResolvers() — it’s native, zero-dependency, and does that one job. Deferred earns its place when you want the extras bundled with it (progress, the iterator, the concurrency helpers) or you need to support environments predating Baseline 2024. And the anti-pattern warning stands: if the work already returns a promise, don’t wrap a Deferred around it — just return the promise.
I got tired of writing the hoisted-resolve trick from scratch in every project, so I made it a dependency instead. Now that the language has its own version, where do you draw the line — reach for native withResolvers and rebuild the helpers each time, or keep them in one small library?
Repo: github.com/Livshitz/concurrency.libx.js npm: concurrency.libx.js
References
- MDN — Promise.withResolvers() (Baseline 2024): developer.mozilla.org
- TC39 — Promise.withResolvers proposal (Stage 4): github.com/tc39/proposal-promise-with-resolvers
- Petka Antonov — Promise anti-patterns (the Deferred anti-pattern), Bluebird wiki: github.com/petkaantonov/bluebird
- jQuery — jQuery.Deferred(): api.jquery.com/jQuery.Deferred </content> </invoke>