Promise.withResolvers()

manage promises from anywhere in your code

Before the introduction of Promise.withResolvers(), there was only one way to create promises directly:


const promise = new Promise((res, rej) => {
	resolve = res;
	reject = rej;
});
					

In most cases, the design of this API is quite acceptable, but it still significantly limits the structure of our asynchronous code. This can be illustrated with an example where we try to resolve* and reject a promise from outside the constructor:


let globalResolve, globalReject;

const promise = new Promise((res, rej) => {
	globalResolve = res;
	globalReject = rej;
});

Math.random() > 0.5 ? globalResolve("Resolved") : globalReject("Rejected");
					

Yes, it works, but it looks a bit inconvenient, especially because we need to declare variables in a wider scope only to reassign them later.

Resolve promises more flexibly

The new method Promise.withResolvers() makes remote resolution of promises more concise. The method returns an object containing: a function to resolve, a function to reject, and the promise itself. This object can be destructured:


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

Math.random() > 0.5 ? resolve("Resolved") : reject("Rejected");

Since they come from the same object, the resolve() and reject() functions are bound to a specific promise, meaning they can be called anywhere. There's no longer a need to stick to the constructor or reassign variables from another scope.

Events Aggregator

The most obvious advantage of withResolvers is that it helps avoid nesting when creating a promise and reduces the amount of boilerplate code.

But it is also useful when you need to create multiple events with resolve/reject. Look at this example of creating an EventsAggregator class. It has an add method to add a new event and an abort method, which cancels aggregation. But most importantly, it returns an event promise that resolves when it reaches the eventsCount limit or rejects when abort is triggered.

						
class EventsAggregator {
	constructor(eventsCount) {
	  this.eventsCount = eventsCount;
	  this.events = [];
	  // Object.assign(this, withResolvers()) -> this.promise, this.resolve, this.reject
	  Object.assign(this, withResolvers());
	}
  
	addEvent(event) {
	  if (this.events.length < this.eventsCount) {
		this.events.push(event);
	  }
	  if (this.events.length === this.eventsCount) {
		this.resolve(this.events);
	  }
	}

	abort() {
	  this.reject("Aborted.");
	}

	get eventsPromise() {
	  return this.promise;
	}
}

const eventsAggregator = new EventsAggregator(3);

eventsAggregator.eventsPromise
	.then((events) => console.log("Resolved: ", events))
	.catch((reason) => console.error("Rejected: ", reason));

eventsAggregator.addEvent("first");
eventsAggregator.addEvent("second");
eventsAggregator.addEvent("third");
// Resolved: [ "first", "second", "third" ]
						
					

If we finish event aggregation by calling the abort method, the promise will be rejected:

						
eventsAggregator.addEvent("first");
eventsAggregator.abort();
eventsAggregator.addEvent("second");
eventsAggregator.addEvent("third");
// Rejected: Aborted.
						
					


withResolvers DIY

WithResolvers appeared in the specification quite recently, so some environments may not support it. Here's how to implement it yourself:

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

Both vanilla promises and Promise.withResolvers() are convenient tools for working with asynchronous code in JS. While traditional promises provide reliable functionality, Promise.withResolvers() offers a more concise way to handle promises.