Implement your own version of Promise.withResolvers — the ES2024 helper that hands you a fresh promise alongside its resolve and reject functions, so you can settle the promise from outside its executor. Before this method existed, you had to declare two let bindings in the outer scope, then assign them inside new Promise(...) — workable, but noisy enough that it became a near-universal helper in every promise-heavy codebase. The new static method does that boilerplate once, correctly, and returns the three things you need as a plain object.
Your job is to implement a function named promiseWithResolvers that returns { promise, resolve, reject }. The resolve and reject functions must be the same ones the promise's executor would have received, so calling them from anywhere settles the promise just as if you'd called them inside the executor.
// Returns a triple: a new pending Promise, plus the resolve and reject
// functions that settle it from outside.
function promiseWithResolvers<T>(): {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: unknown) => void;
};
// Basic resolve from outside the executor.
const { promise, resolve } = promiseWithResolvers();
setTimeout(() => resolve(42), 10);
promise.then(console.log); // logs 42 after ~10ms
// Basic reject.
const { promise: p2, reject } = promiseWithResolvers();
reject(new Error('nope'));
p2.catch((e) => console.log(e.message)); // logs "nope"
// Event-handler bridge: turn a one-shot DOM event into an awaitable.
function nextClick(button) {
const { promise, resolve } = promiseWithResolvers();
button.addEventListener('click', resolve, { once: true });
return promise;
}
// Caller can now: const event = await nextClick(myButton);
{ promise, resolve, reject }. Test code destructures these keys by name.resolve and reject you return must be the actual functions the executor was called with — not wrappers around them. Calling resolve(value) from outside must fulfil promise with value.resolve (or a reject after resolve) is silently ignored. You don't need to enforce this yourself — the native Promise constructor already does.Promise constructor. You are building a small adapter on top of new Promise(...). You are not writing a promise implementation from scratch.Promise.withResolvers.call(MyPromise)), prototype pollution, or non-Promise return types. Always return a plain object with a real Promise instance.