ES2024 & ES2025 Features
intermediateObject.groupBy, Promise.withResolvers, Set methods, iterator helpers, and the other additions worth knowing in interviews.
Learn Promise.withResolvers — the ES2024 built-in deferred — how to polyfill it, and when settling a promise from outside is the right design.
The Promise constructor forces you to decide how the promise settles inside the executor:
const promise = new Promise((resolve, reject) => {
// resolve/reject only usable in here
});But real code often needs to settle a promise from somewhere else — an event handler, a message from a worker, a queue consumer. For years everyone wrote the same "deferred" workaround by smuggling the functions out:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});ES2024 standardizes exactly this as Promise.withResolvers:
const { promise, resolve, reject } = Promise.withResolvers();if (!Promise.withResolvers) {
Promise.withResolvers = function () {
let resolve, reject;
const promise = new this((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
}Two details worth saying out loud in an interview:
resolve/reject are guaranteed to be assigned before the return — no race.new this(...) instead of new Promise(...) keeps it subclass-friendly (MyPromise.withResolvers() returns a MyPromise).function waitForMessage(worker, matchId) {
const { promise, resolve } = Promise.withResolvers();
worker.addEventListener("message", function handler(e) {
if (e.data.id === matchId) {
worker.removeEventListener("message", handler);
resolve(e.data);
}
});
return promise;
}class TaskQueue {
#tasks = [];
enqueue(run) {
const { promise, resolve, reject } = Promise.withResolvers();
this.#tasks.push({ run, resolve, reject });
this.#drain();
return promise; // caller awaits completion of their specific task
}
async #drain() {
if (this.#draining) return;
this.#draining = true;
while (this.#tasks.length) {
const { run, resolve, reject } = this.#tasks.shift();
try { resolve(await run()); } catch (e) { reject(e); }
}
this.#draining = false;
}
#draining = false;
}This is the core of the promise pool / concurrency-limit question, where deferreds let queued callers receive their own results.
Combine a deferred with AbortController to build sleep that can be interrupted:
function sleep(ms, signal) {
const { promise, resolve, reject } = Promise.withResolvers();
const id = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => {
clearTimeout(id);
reject(signal.reason);
});
return promise;
}A deferred is an escape hatch, not a default. If the entire lifecycle lives in one place, the plain constructor (or just async/await) is clearer and keeps settle-rights private. Handing resolve around widens who can settle the promise — the same reasoning behind "don't expose your setState to strangers." Saying this trade-off unprompted is what makes the answer senior.
ready/closed promises, module loaders, and most test frameworks' expect.poll-style helpers.Object.groupBy, Promise.withResolvers, Set methods, iterator helpers, and the other additions worth knowing in interviews.
map, filter, take, and drop directly on iterators (ES2025): lazy evaluation without loading everything into an array.