Asynchronous
intermediate
#promises#async#withResolvers

Understanding Promise.withResolvers in JavaScript

Discover Promise.withResolvers, the ES-2024 addition that gives you the promise and its resolve / reject functions in one line. Perfect for event emitters, timeouts, resource pools and more.

June 25, 2025
5 min read
Share this article:

Creating Promises from scratch in JavaScript has traditionally required a somewhat awkward pattern:

javascript
let resolvePromise, rejectPromise
const promise = new Promise((resolve, reject) => {
  resolvePromise = resolve
  rejectPromise = reject
})

The new Promise.withResolvers() method provides a cleaner solution to this common scenario. Let's explore how it works and where it's most useful.

The Basics

Promise.withResolvers() returns an object containing both a Promise and its controlling functions:

javascript
const { promise, resolve, reject } = Promise.withResolvers()

This simple change eliminates the need for closure variables and makes the code's intent clearer. Let's look at some practical applications.

Practical Applications

Building an Event System

Here's how you might build a simple event system that converts callbacks to promises:

javascript
class AsyncEventEmitter {
  constructor() {
    this.listeners = new Map()
  }

  once(eventName) {
    const { promise, resolve } = Promise.withResolvers()
    this.listeners.set(eventName, resolve)
    return promise
  }

  emit(eventName, data) {
    const resolver = this.listeners.get(eventName)
    if (resolver) {
      resolver(data)
      this.listeners.delete(eventName)
    }
  }
}

// Usage example
const emitter = new AsyncEventEmitter()
async function handleEvent() {
  const data = await emitter.once('user-action')
  console.log('User action received:', data)
}

Implementing Request Timeouts

Here's a practical implementation of a timeout mechanism for async operations:

javascript
function createTimeout(ms) {
  const { promise, reject } = Promise.withResolvers()
  const timer = setTimeout(() => {
    reject(new Error('Operation timed out'))
  }, ms)

  return {
    promise,
    clear: () => clearTimeout(timer),
  }
}

async function fetchWithTimeout(url, ms) {
  const timeout = createTimeout(ms)
  try {
    const response = await Promise.race([fetch(url), timeout.promise])
    timeout.clear()
    return response
  } catch (error) {
    timeout.clear()
    throw error
  }
}

Managing Resource Pools

Here's how you might implement a simple resource pool:

javascript
class ResourcePool {
  constructor(resources) {
    this.available = [...resources]
    this.waitQueue = []
  }

  async acquire() {
    if (this.available.length > 0) {
      return this.available.pop()
    }

    const { promise, resolve } = Promise.withResolvers()
    this.waitQueue.push(resolve)
    return promise
  }

  release(resource) {
    if (this.waitQueue.length > 0) {
      const nextUser = this.waitQueue.shift()
      nextUser(resource)
    } else {
      this.available.push(resource)
    }
  }
}

Implementation Details

The advantages of using Promise.withResolvers() include:

  1. Cleaner Scope: No need for variables in outer scope
  2. Type Safety: Better TypeScript integration with proper type inference
  3. Readability: The code's intent is immediately clear
  4. Reliability: Eliminates potential mistakes in resolver assignment

Browser Support

This feature was standardised in ECMAScript 2024 (see ES-2026 draft § 27.7.1). It is available in Chrome 117+, Firefox 119+, Safari 17.4+ and Node 20.12+. For runtimes that don't yet support it, here's a minimal polyfill:

javascript
if (!Promise.withResolvers) {
  Promise.withResolvers = function () {
    let resolve, reject
    const promise = new Promise((res, rej) => {
      resolve = res
      reject = rej
    })
    return { promise, resolve, reject }
  }
}

Best Practices

When working with Promise.withResolvers(), consider these guidelines:

  1. Memory Management: The Promise and its resolver functions remain in memory until resolved or rejected. Clean up appropriately in long-running operations.

  2. Error Handling: Always implement proper error handling through the reject function or try/catch blocks.

  3. Promise Chaining: The returned promise supports all standard Promise operations (then, catch, finally).

Use Cases

Promise.withResolvers() is most valuable when:

  • Building custom asynchronous control flow
  • Implementing event-based systems
  • Creating timeout or cancellation mechanisms
  • Managing resource pools
  • Handling external events in a Promise-based way

However, avoid using it when:

  • Simple async/await syntax would suffice
  • You're just wrapping a callback API (use util.promisify instead)
  • The Promise resolution doesn't need external control

Conclusion

Promise.withResolvers() streamlines a common JavaScript pattern, making it easier to write clean, maintainable asynchronous code. While it's not needed for every Promise-based operation, it's a valuable tool for scenarios requiring external Promise control.