Skip to content

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);     // 42

Implementation

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/finally delegate to the inner promise, so DeferredPromise is thenable — it works with await
  • Status tracking lets you check synchronously whether the promise has settled, which standard promises don't allow

Trade-offs

ApproachProsCons
DeferredPromiseExternal control, statusExtra class
Promise.withResolvers()Native, simpleNo status tracking
new Promise(executor)StandardTrapped 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.