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.
Creating Promises from scratch in JavaScript has traditionally required a somewhat awkward pattern:
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:
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:
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:
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:
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:
- Cleaner Scope: No need for variables in outer scope
- Type Safety: Better TypeScript integration with proper type inference
- Readability: The code's intent is immediately clear
- 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:
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:
-
Memory Management: The Promise and its resolver functions remain in memory until resolved or rejected. Clean up appropriately in long-running operations.
-
Error Handling: Always implement proper error handling through the reject function or try/catch blocks.
-
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.