JS
() =>
JavaScript · ES2024 · Promise API
Promise.withResolvers()
Das Deferred Pattern nativ und ohne Hilfsklasse

Wer einen Promise von außen auflösen wollte – aus einem Event-Handler, einer Callback-API oder einem anderen asynchronen Kontext –, musste bisher resolve und reject aus dem Konstruktor-Scope herausschmuggeln. Promise.withResolvers() löst das in einer Zeile: { promise, resolve, reject } als strukturiertes Objekt, klar und ohne Boilerplate.

9 Min. Lesezeit Promise.withResolvers · Deferred Pattern · Async/Await · Event-Queue Chrome 119+ · Firefox 121+ · Safari 17.4+ · Node.js 22+

1. Das klassische Problem: resolve außerhalb des Konstruktors

Das Grundproblem, das Promise.withResolvers() löst, ist so alt wie Promises in JavaScript selbst. Der new Promise(executor)-Konstruktor ruft seinen Callback synchron auf und stellt resolve und reject nur innerhalb dieses Callbacks bereit. Sobald man einen Promise erstellt und dessen Auflösung an einen externen Event-Handler delegieren will – etwa einen WebSocket-Message-Event, ein DOM-Event oder eine Legacy-Callback-API –, musste man bisher die Funktionen aus dem Executor-Scope in eine äußere Variable herausschmuggeln.

Das klassische Muster sieht so aus: let resolve; const p = new Promise(r => { resolve = r; });. Das ist funktional korrekt, aber implizit, fehleranfällig und erzeugt TypeScript-Warnungen, weil resolve vor der Zuweisung als undefined angesehen wird. Jedes Team, das dieses Muster öfter braucht, schreibt eine Deferred-Hilfsklasse. Mit Promise.withResolvers() ist diese Klasse überflüssig: Die Sprache stellt das Muster nativ bereit.

2. Syntax und Rückgabewert von Promise.withResolvers()

Promise.withResolvers() ist eine statische Methode auf Promise und nimmt keine Argumente. Sie gibt ein Objekt mit drei Eigenschaften zurück: promise, resolve und reject. Das promise-Objekt ist ein regulärer, unaufgelöster Promise. Die Funktionen resolve und reject sind die entsprechenden Auflösungsfunktionen, die den Promise in den fulfilled oder rejected Zustand versetzen. Sie haben dieselbe Semantik wie die Parameter des normalen Promise-Konstruktors – mehrfaches Aufrufen von resolve nach dem ersten Mal hat keinen Effekt.

Das zurückgegebene Objekt ist ideal für Destructuring: const { promise, resolve, reject } = Promise.withResolvers(); – drei Zeilen Code, die früher eine Klasse oder einen Boilerplate-Block erforderten, werden zu einer einzigen. Die drei Variablen können jetzt unabhängig voneinander an verschiedene Teile des Programms weitergegeben werden: promise an den Konsumenten, resolve an den Event-Handler, reject an den Fehlerbehandlungs-Code. Das ist genau das Deferred-Muster, nur ohne Klasse.


// Promise.withResolvers() — one-liner for the Deferred pattern
const { promise, resolve, reject } = Promise.withResolvers();

// promise is a standard Promise — consumers await or .then() it
promise.then(value  => console.log("Resolved:", value));
promise.catch(error => console.error("Rejected:", error));

// resolve and reject can be called from anywhere, any time
setTimeout(() => resolve("Hello from the future!"), 1000);
// After 1 second: "Resolved: Hello from the future!"

// Compare: old boilerplate (still works, but verbose)
let _resolve, _reject;
const oldPromise = new Promise((res, rej) => {
  _resolve = res; // leak from executor scope
  _reject  = rej;
});
// _resolve is potentially undefined before the sync executor runs
// TypeScript requires '!' or conditional check — messy

3. Das Deferred Pattern: Was es ist und warum es zurückgekehrt ist

Das Deferred-Pattern ist ein klassisches Entwurfsmuster für asynchrone Programmierung, das in der JavaScript-Community durch jQuery's $.Deferred() bekannt wurde, lange bevor native Promises existierten. Die Grundidee: Ein "Deferred"-Objekt kapselt einen Promise zusammen mit seinen Auflösungsfunktionen, so dass der Promise von außen kontrolliert werden kann – vom Ersteller getrennt von der auflösenden Stelle. Mit nativen Promises verschwand das Muster aus der offiziellen API, weil der Konstruktor eine eingebaute Alternative war – eine unbequeme, aber technisch ausreichende.

Dass das TC39-Komitee das Deferred-Pattern mit Promise.withResolvers() explizit wieder in die Sprache aufnimmt, ist ein klares Signal: Der Anwendungsfall ist real und häufig genug, um native Unterstützung zu rechtfertigen. Die Erkenntnis dahinter: Viele asynchrone Szenarien in modernem JavaScript – Event-Queues, Stream-Bridging, Koordination mehrerer async-Operationen – erfordern einen Promise, dessen Auflösung an einer anderen Stelle im Code liegt als seine Erstellung. Das Deferred-Pattern ist die sauberste Lösung dafür, und Promise.withResolvers() macht es zur ersten Wahl.

4. Anwendungsfall: Event-Queues und einmalige Events bridgen

Der wichtigste Anwendungsfall von Promise.withResolvers() ist das Bridging von Event-basierten APIs auf Promise-basierte. Ein häufiges Szenario: Man will auf das erste Auftreten eines DOM-Events oder eines Node.js-EventEmitter-Ereignisses warten und dieses als auflösbaren Promise darstellen. Mit Promise.withResolvers() kann man eine saubere once(emitter, event)-Hilfsfunktion schreiben, die genau das tut – ohne Klassen, ohne komplexen Lifecycle-Code.

Für komplexere Szenarien – etwa eine Warteschlange, bei der jeder Eintrag durch einen eigenen Promise.withResolvers()-Aufruf kontrolliert wird – entsteht eine reaktive Datenstruktur, die beliebig viele asynchrone Konsumenten koordiniert. Das ist das Fundament für async-iterable Queues, "Channel"-Konzepte aus der Go-Welt in JavaScript und generelle Produzenten/Konsumenten-Architekturen. Statt mit komplexen RxJS-Operatoren oder dritt-Bibliotheken kann man solche Muster mit Promise.withResolvers() und etwas Array-Logik direkt in native JavaScript implementieren.


// Bridge EventEmitter to Promise with Promise.withResolvers()
function once(emitter, event) {
  const { promise, resolve, reject } = Promise.withResolvers();

  emitter.addEventListener(event,  resolve, { once: true });
  emitter.addEventListener("error", reject,  { once: true });

  return promise;
}

// Usage: await a DOM event as a Promise
const button = document.querySelector("#submit");
const clickEvent = await once(button, "click");
console.log("Button clicked:", clickEvent.target.id);

// Async-iterable queue using Promise.withResolvers()
class AsyncQueue {
  #queue  = [];
  #waiters = [];

  enqueue(value) {
    if (this.#waiters.length > 0) {
      // Resolve the oldest waiting consumer directly
      this.#waiters.shift().resolve(value);
    } else {
      this.#queue.push(value);
    }
  }

  dequeue() {
    if (this.#queue.length > 0) {
      return Promise.resolve(this.#queue.shift());
    }
    // No item available — create a Deferred and wait
    const deferred = Promise.withResolvers();
    this.#waiters.push(deferred);
    return deferred.promise;
  }
}

const queue = new AsyncQueue();
// Consumer: waits until an item is available
const item = await queue.dequeue(); // suspends until enqueue() is called

5. Timeout und Abbruch-Mechanismen mit withResolvers()

Promise.withResolvers() vereinfacht die Implementierung von Timeout-Wrappern erheblich. Das klassische Muster – Promise.race([originalPromise, new Promise(r => setTimeout(() => r("timeout"), ms))]) – ist korrekt, aber hat eine Schwäche: der Timeout-Promise kann nicht aufgeräumt werden, wenn das Original zuerst auflöst. Mit Promise.withResolvers() kann man den Timeout-Handle festhalten und clearTimeout aufrufen, sobald der originale Promise auflöst, bevor der Timeout feuert.

In Kombination mit AbortController und AbortSignal entsteht ein vollständiger Abbruch-Mechanismus: Der AbortSignal löst bei Abbruch das abort-Event aus, was über einen Event-Listener direkt reject(signal.reason) aufruft – sauber, ohne Boilerplate, mit echtem Ressourcen-Cleanup. Diese Kombination ist das moderne Muster für abbrechbare asynchrone Operationen in JavaScript und ersetzt ältere, komplexere Ansätze mit CancellationToken-Hilfsklassen.


// Timeout wrapper with Promise.withResolvers() — clean timer cancel
function withTimeout(promise, ms) {
  const { promise: timeoutP, resolve, reject } = Promise.withResolvers();

  const timer = setTimeout(
    () => reject(new Error(`Timed out after ${ms} ms`)),
    ms
  );

  // Race the original promise against the timeout
  return Promise.race([
    promise.finally(() => clearTimeout(timer)), // cancel timer on settle
    timeoutP,
  ]);
}

// AbortSignal integration — cancel via controller
function fetchWithAbort(url, signal) {
  const { promise, resolve, reject } = Promise.withResolvers();

  // Reject immediately if already aborted
  if (signal.aborted) {
    reject(signal.reason);
    return promise;
  }

  // Reject on abort event
  signal.addEventListener("abort", () => reject(signal.reason), { once: true });

  // Resolve on successful fetch
  fetch(url, { signal })
    .then(r => r.json())
    .then(resolve)
    .catch(reject);

  return promise;
}

const controller = new AbortController();
setTimeout(() => controller.abort(new Error("User cancelled")), 3000);
const data = await fetchWithAbort("/api/data", controller.signal);

6. Subclassing: Promise.withResolvers() auf Custom-Promise-Klassen

Ein weniger bekanntes Feature von Promise.withResolvers(): Als statische Methode berücksichtigt sie das Symbol.species-Muster und den this-Kontext, auf dem sie aufgerufen wird. Das bedeutet, dass MyCustomPromise.withResolvers() ein MyCustomPromise-Objekt zurückgibt, nicht ein natives Promise. Wer also eine Custom-Promise-Klasse mit erweiterter Semantik (z. B. Cancellation, Logging oder Retry-Logik) implementiert, kann Promise.withResolvers() direkt auf der Subklasse aufrufen und erhält einen voll funktionsfähigen Deferred des richtigen Typs.

Das macht Promise.withResolvers() nicht nur für einfache Anwendungen nützlich, sondern auch für Bibliotheken, die eigene Promise-Implementierungen anbieten. Die Methode ist explizit so spezifiziert, dass sie den korrekten this-Kontext nutzt: new this(…) statt new Promise(…). Das ist derselbe Mechanismus, den Promise.all(), Promise.race() und andere statische Methoden verwenden, um Subklassen korrekt zu unterstützen.

7. Fallstricke: Mehrfach-Resolve, unhandled Rejection und Memory-Leaks

Der erste Fallstrick mit Promise.withResolvers() – und Promises generell – ist das mehrfache Aufrufen von resolve() oder reject(). Nach dem ersten Aufruf ist der Promise festgelegt (settled) und jede weitere Auflösung wird ignoriert. Es gibt keinen Fehler, keine Warnung – die nachfolgenden Aufrufe verschwinden still. Wer darauf angewiesen ist, zu wissen, ob ein Promise bereits aufgelöst wurde, muss selbst eine State-Variable führen oder eine Bibliothek verwenden, die Mehrfach-Auflösung erkennt.

Der zweite Fallstrick ist die unhandled Rejection: Wenn reject() aufgerufen wird und kein .catch()-Handler am Promise hängt, erzeugt das einen unhandled rejection warning in Node.js und einen Konsolenfehler im Browser. Mit Promise.withResolvers() ist die Gefahr höher, weil promise, resolve und reject jetzt getrennte Variablen sind und das promise möglicherweise nicht an alle Stellen weitergegeben wird, wo es consumed werden sollte. Memory-Leaks entstehen, wenn ein Promise niemals aufgelöst wird und gleichzeitig Event-Listener, die resolve halten, nicht entfernt werden – der Promise und die Listener halten sich gegenseitig am Leben.

Muster Code-Aufwand TypeScript-sicher Empfehlung
Promise.withResolvers() 1 Zeile Ja, nativ Standard ab ES2024
Konstruktor-Escape 3–4 Zeilen + let Typ-Assertion nötig Vermeiden in neuem Code
Deferred-Klasse 10–20 Zeilen Klasse Ja, wenn generisch Nur für Legacy-Projekte
Bibliothek (p-defer etc.) npm install + import Ja Nicht mehr nötig ab ES2024

9. TypeScript-Integration: Typisierung und Generic-Verwendung

Promise.withResolvers() ist seit TypeScript 5.4 vollständig typisiert. Der Rückgabetyp ist PromiseWithResolvers<T>, ein Interface mit den drei Eigenschaften promise: Promise<T>, resolve: (value: T | PromiseLike<T>) => void und reject: (reason?: unknown) => void. TypeScript inferiert den Typ T aus dem Verwendungskontext oder erwartet eine explizite Typ-Angabe: Promise.withResolvers<string>().

Ein häufiger Anwendungsfall in TypeScript: Eine Funktion gibt ein Promise<T> zurück, muss es aber in einer anderen Methode derselben Klasse auflösen. Statt eine komplizierte State-Machine mit internen Callbacks zu bauen, hält die Klasse einfach eine PromiseWithResolvers<T>-Instanz als Property und ruft this.deferred.resolve(value) auf, wenn der Wert verfügbar ist. Das macht den Code linear, leicht verständlich und vollständig typsicher – ohne zusätzliche Bibliotheken oder manuelle Typ-Definitionen.

Mironsoft

Async-Architektur und TypeScript-Entwicklung für skalierbare Anwendungen

Komplexe async-Architektur vereinfachen?

Wir refactoren Legacy-Callback-Code auf moderne Promise-Patterns mit Promise.withResolvers(), async/await und AbortController – für wartbaren, typsicheren JavaScript-Code.

Async-Audit

Analyse von Callback-Hölle, unkontrollierten Promises und unhandled Rejections

Refactoring

Migration auf Promise.withResolvers(), AbortController und async iterables

TypeScript

Vollständige Typisierung von async-Code mit PromiseWithResolvers und generischen Typen

10. Zusammenfassung

Promise.withResolvers() ist eine scheinbar kleine, aber in der Praxis bedeutende Ergänzung in ES2024. Sie eliminiert den häufig genutzten Boilerplate-Trick, resolve und reject aus dem Promise-Konstruktor-Scope zu leaken, und macht das Deferred-Pattern zu einem First-Class-Bürger der JavaScript-Standardbibliothek. Das Ergebnis: klarerer Code für Event-Bridging, Timeout-Wrapper, Async-Queues und abbrechbare Operationen – ohne externe Bibliotheken und ohne Hilfsklassen.

Die wichtigsten Punkte im Rückblick: Promise.withResolvers() gibt { promise, resolve, reject } zurück. Mehrfaches Auflösen wird still ignoriert. Bei Subklassen wird der korrekte Typ über this inferiert. TypeScript unterstützt die Methode vollständig seit Version 5.4. Für moderne Projekte mit Chrome 119+, Firefox 121+ und Node.js 22+ ist kein Polyfill erforderlich.

Promise.withResolvers() — Das Wichtigste auf einen Blick

Rückgabewert

{ promise, resolve, reject } – drei unabhängige Variablen, die an verschiedene Teile des Codes weitergegeben werden können.

Deferred Pattern

Kapselt Promise-Auflösung von der Promise-Erstellung. Ideal für Event-Bridging, Queues, Timeout-Wrapper und abbrechbare Operationen.

Fallstricke

Mehrfach-Resolve ist still. Unhandled Rejection wenn .catch() fehlt. Memory-Leak wenn nicht aufgelöste Promises Event-Listener halten.

Kompatibilität

Chrome 119+, Firefox 121+, Safari 17.4+, Node.js 22+. TypeScript ab 5.4. Kein Polyfill für moderne Targets nötig.

11. FAQ: Promise.withResolvers() in JavaScript

1Was ist Promise.withResolvers()?
Statische ES2024-Methode, gibt { promise, resolve, reject } zurück. Macht resolve/reject außerhalb des Konstruktors zugänglich – nativ, ohne Hilfsklasse.
2Was ist das Deferred Pattern?
Promise + Auflösungsfunktionen zusammen kapseln. Auflösung von anderer Stelle im Code möglich als Erstellung – klassisches async-Muster.
3Wann verwenden?
Event-Bridging, Timeout-Wrapper, Async-Queues, abbrechbare Operationen – immer wenn Auflösung von außerhalb des Konstruktors nötig ist.
4Mehrfaches resolve()?
Nur erster Aufruf wirkt. Weitere werden still ignoriert – kein Fehler. Für Erkennung eigene State-Variable führen.
5Unterschied zur Deferred-Klasse?
Funktional identisch. Promise.withResolvers() ersetzt die Klasse durch einen Einzeiler – nativ, typisiert, ohne externe Abhängigkeit.
6TypeScript-Support?
Ab TypeScript 5.4. Rückgabetyp: PromiseWithResolvers<T>. Generics: Promise.withResolvers<string>(). T wird aus dem Kontext inferiert.
7Memory-Leaks vermeiden?
{ once: true } bei Event-Listenern oder explizit removeEventListener(). Nie aufgelöste Promises sind GC-Kandidaten sobald alle Referenzen weg sind.
8Kombination mit AbortController?
Ja. signal.addEventListener("abort", () => reject(signal.reason), { once: true }). Sauberer Abbruch-Mechanismus ohne Bibliothek.
9Funktioniert mit Subklassen?
Ja. MyCustomPromise.withResolvers() gibt eine MyCustomPromise-Instanz zurück – this wird als Konstruktor verwendet.
10Browser-Support?
Chrome 119, Firefox 121, Safari 17.4, Node.js 22. Einfacher Polyfill mit Konstruktor-Escape-Muster für ältere Targets.