Appearance
Deferred Promise
A promise whose resolve and reject functions are accessible from outside the executor callback. Useful when the code that creates the promise is separate from the code that settles it.
When to Use
- Bridging callback-based APIs to promises
- Exposing resolve/reject to a different scope (e.g., a class managing async state)
- Building custom async primitives like locks, queues, or timers
- Inspecting promise status synchronously (pending, fulfilled, rejected)
The Pattern
Basic usage
javascript
const deferred = new DeferredPromise();
// Somewhere else in the code
setTimeout(() => deferred.resolve("done"), 1000);
const result = await deferred;
console.log(result); // "done"Class-level async state
javascript
class Connection {
#ready = new DeferredPromise();
async connect(url) {
try {
await this.#open(url);
this.#ready.resolve();
} catch (error) {
this.#ready.reject(error);
}
}
async query(sql) {
await this.#ready;
return this.#execute(sql);
}
}Synchronous status checks
javascript
const deferred = new DeferredPromise();
console.log(deferred.settled); // false
deferred.resolve(42);
// After microtask
console.log(deferred.fulfilled); // true
console.log(deferred.value); // 42Implementation
javascript
class DeferredPromise {
#status = "pending";
#promise = null;
#resolve = null;
#reject = null;
#value = undefined;
#reason = undefined;
constructor() {
const { promise, resolve, reject } = Promise.withResolvers();
this.#promise = promise;
this.#resolve = resolve;
this.#reject = reject;
promise.then(
(value) => {
this.#status = "fulfilled";
this.#value = value;
},
(reason) => {
this.#status = "rejected";
this.#reason = reason;
},
);
}
get promise() {
return this.#promise;
}
get fulfilled() {
return this.#status === "fulfilled";
}
get rejected() {
return this.#status === "rejected";
}
get settled() {
return this.fulfilled || this.rejected;
}
get value() {
return this.#value;
}
get reason() {
return this.#reason;
}
resolve(value) {
this.#resolve(value);
}
reject(reason) {
this.#reject(reason);
}
then(onFulfilled, onRejected) {
return this.promise.then(
onFulfilled, onRejected,
);
}
catch(onRejected) {
return this.promise.then(undefined, onRejected);
}
finally(onSettled) {
return this.promise.then(
(value) => Promise.resolve(onSettled())
.then(() => value),
(reason) => Promise.resolve(onSettled())
.then(() => { throw reason; }),
);
}
}Key details
- Built on
Promise.withResolvers()(native in modern runtimes) - Private fields prevent external tampering with state
then/catch/finallydelegate to the inner promise, soDeferredPromiseis thenable — it works withawait- Status tracking lets you check synchronously whether the promise has settled, which standard promises don't allow
Trade-offs
| Approach | Pros | Cons |
|---|---|---|
DeferredPromise | External control, status | Extra class |
Promise.withResolvers() | Native, simple | No status tracking |
new Promise(executor) | Standard | Trapped in closure |
Prefer Promise.withResolvers() for simple cases where you just need external resolve/reject. Use DeferredPromise when you also need status inspection or a reusable thenable object.