Streamlining Event Handling with Promise.withResolvers()

Say goodbye to deep nested event handling in JavaScript.

Streamlining Event Handling with Promise.withResolvers()

Wrestling with nested callbacks and event listeners when handling asynchronous events in JavaScript can make your code a bit messy. The new Promise.withResolvers() method, on track for inclusion in ES2024 and already available in Firefox 121, offers a cleaner way to manage these situations. Let's explore how!

The Challenge of Event-Driven Promises

As JavaScript developers, we frequently handle events such as button clicks, network responses, and more. In JS, asynchronous processing APIs have been established based on event listeners and processed using flow control to combine them.

However, since the introduction of the Promise, it has become more common for standard APIs to return Promises and handle them with async/ await. As a result, the number of cases where event listener-based functions are being converted to Promises has increased.

Here's a common pattern that can lead to nesting when wrapping events in Promises:

async function request() {
  return new Promise((resolve, reject) => {
    document.querySelector("button").addEventListener("click", async () => {
      try {
        const res = await fetch("/")
        const body = await res.text()
        resolve(body)
      } catch (err) {
        reject(err)
      }
    })
  })
}

event-driven promise

Notice how the resolve and reject functions, which control the Promise, are nested within the event listener callback.

Managing resolve and reject

To make the code less nested, we sometimes extract the resolve and reject management:

async function request() {
  let resolve, reject
  const promise = new Promise((res, rej) => {
    resolve = res
    reject = rej
  })
  document.querySelector("button").addEventListener("click", async () => {
    try {
      const res = await fetch("/")
      const body = res.text()
      resolve(body)
    } catch (err) {
      reject(err)
    }
  })
  return promise
}

Since this pattern is common, let's formalize this extraction:

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

Enter Promise.withResolvers()

This is precisely what Promise.withResolvers() standardizes! Here's how the example looks with it:

async function request() {
  const { promise, resolve, reject } = Promise.withResolvers()
  document.querySelector("button").addEventListener("click", async () => {
    try {
      const res = await fetch("/")
      const body = res.text()
      resolve(body)
    } catch (err) {
      reject(err)
    }
  })
  return promise
}

Firefox 121 and PromiseUtils.defer()

As of Firefox 121, Promise.withResolvers() has officially landed! It replaces the functionality of Firefox's PromiseUtils.defer(). Make sure to update your code accordingly if you are using the older utility.

Additional Resources

To dive deeper, check out the official resources:

If you often wrap event-based actions in Promises, Promise.withResolvers() will be a welcome addition to your JavaScript toolkit. It simplifies your asynchronous code, making it more readable and maintainable.