Asynchronous JavaScript often needs a promise whose completion is controlled somewhere else. Until recently, the common pattern was to declare resolve and reject variables outside a new Promise() constructor, then assign the executor’s callbacks to them. That works, but it separates declaration from initialization and can look mysterious to readers. Promise.withResolvers() gives the platform a standard, compact way to create a promise together with its resolve and reject functions.
What Promise.withResolvers returns
The method returns a plain object with three properties: promise, resolve, and reject. The promise behaves like any other promise, while the two functions settle it. Calling resolve(value) fulfills the promise or adopts another promise-like value. Calling reject(reason) rejects it. Later calls have no effect after settlement. The important improvement is structural: the promise and its controls are created together, so there is no temporary uninitialized state and no need to expose executor mechanics.
Why externally controlled promises exist
Most application code should still prefer promises returned directly by an operation. Fetching data, reading a stream, or awaiting a timer is clearest when the API creates and settles its own promise. Externally controlled promises are useful when completion arrives through a different channel: an event listener, a worker message, a queue consumer, a test harness, or a callback-based interface. In those cases, code may need to hand a promise to one part of the system while another part retains the ability to settle it.
A queue example
Consider a small asynchronous queue. Consumers call next() and wait for an item. If the queue is empty, next() can create a resolver record and store it. When a producer later adds data, it takes the oldest record and calls its resolve function. The consumer receives the item without polling. Promise.withResolvers() makes the record explicit and avoids wrapping every wait in an executor that immediately exports its callbacks. This pattern can also support rejection when the queue closes or encounters an unrecoverable error.
Keep ownership clear
The method is convenient, but convenience can encourage promises that are settled from too many places. Give each resolver record a clear owner. Store it in a private field or a tightly scoped collection rather than attaching it to a broad global object. Document which events fulfill it and which conditions reject it. If cancellation is required, define how cancellation interacts with settlement. Clear ownership prevents double-settlement attempts, forgotten waiters, and memory leaks caused by resolver records that remain in a collection forever.
Errors and cleanup
Reject with an Error object when possible so stack information and error handling remain useful. Remove event listeners and references when the promise settles. A finally() handler can centralize cleanup, but be careful not to create a second unobserved rejection chain. When integrating with an AbortSignal, check whether the signal is already aborted before registering a listener, then remove the listener after settlement. The promise itself does not cancel underlying work; cancellation remains a separate protocol that your code must implement.
Relationship to scheduling
A resolver function does not run waiting code synchronously. Promise reactions are queued as microtasks, preserving normal promise semantics. That detail matters when a resolver is called inside an event handler or task queue. For broader responsiveness work, see Noorani’s article on scheduler.yield(). For task prioritization, the guide to Prioritized Task Scheduling explains how browser tasks can be organized without confusing task queues with promise settlement.
Streams and repeated values
A single promise represents one eventual settlement, so repeated values require a sequence of promises or a stream. A queue may create a fresh resolver record after every delivered item. However, if the problem naturally represents continuous data, a ReadableStream may provide clearer backpressure and lifecycle controls. Noorani’s WebSocketStream guide shows why backpressure matters in real-time work. Choose the abstraction that describes the data: one result, a series of results, or an ongoing flow.
Compatibility and fallbacks
Check the browsers and JavaScript runtimes your project supports before shipping the method without a fallback. The older constructor pattern can be wrapped in a tiny helper with the same returned shape, making migration straightforward. Keep that helper local and well tested. Avoid downloading a large dependency for such a small behavior. When build tools target older environments, confirm whether they provide a runtime polyfill; syntax transformation alone cannot add a missing built-in method.
Authoritative references
The behavior is standardized in the ECMAScript specification section for Promise.withResolvers. For an implementation-oriented explanation, see the V8 feature article. These primary sources describe the returned record, subclass behavior, and practical motivation without relying on framework-specific conventions.
When to use it
Use Promise.withResolvers() when an operation’s consumer needs a promise now but the result will arrive through a separately owned event or queue. Do not use it simply to make ordinary promise code look more advanced. If a function can return the promise produced by its work, that is usually simpler. The new method is most valuable at coordination boundaries, where it replaces a clumsy constructor pattern with a named, readable primitive.
Browse with calm, clarity, and purpose
Download Noorani Browser for a focused browsing experience with thoughtful privacy tools and useful everyday features.
