Streamlining Event Handling with Promise.withResolvers()
Say goodbye to deep nested event handling in JavaScript.
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:
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:
- Spec: https://tc39.es/proposal-promise-with-resolvers/
- Explainer: https://github.com/tc39/proposal-promise-with-resolvers
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.