JavaScript Promisification
JavaScript Promisification: Callbacks in Promises umwandeln, einen generischen promisify()-Helfer erstellen, Mehrfarg-Callbacks behandeln und Grenzen kennen.
Was ist Promisification?
Promisification bezeichnet das Umhüllen einer Callback-basierten Funktion, sodass sie statt eines Callbacks ein Promise zurückgibt. Man macht es einmal, und kann danach .then(), .catch(), Chaining und async/await auf einer Funktion verwenden, die ursprünglich nicht dafür konzipiert war.
Diese Seite erklärt, wie man eine einzelne Error-First-Callback-API einwickelt, wie man einen wiederverwendbaren generischen promisify()-Helfer erstellt, wie man Callbacks mit mehr als einem Ergebnis behandelt und in welchen Fällen Promisification nicht funktioniert.
Warum Promisify?
Ältere JavaScript-APIs und der Großteil der Node.js-Standardbibliothek liefern ihre Ergebnisse über einen übergebenen Callback. Dieser Stil verschachtelt sich schnell und verstreut die Fehlerbehandlung:
getUser(id, (err, user) => {
if (err) return handleError(err);
getOrders(user, (err, orders) => {
if (err) return handleError(err);
getTotal(orders, (err, total) => {
if (err) return handleError(err);
console.log(total);
});
});
});Wenn dieselben Funktionen Promises zurückgäben, würde die Logik zu einer einzigen linearen Kette (oder einigen await-Zeilen) mit einem einzigen .catch() für den gesamten Ablauf flach. Promisification ist die Brücke zwischen diesen beiden Welten — siehe Callbacks and Beyond für die Callback-Seite der Geschichte.
Die Error-First-Callback-Konvention
Bevor man etwas einwickelt, muss man die Form kennen, die man einwickelt. Node.js-Callbacks folgen der Error-First- (oder „Node-Style"-)Konvention: Der Callback ist das letzte Argument und wird als callback(error, result) aufgerufen.
- Bei einem Fehler ist
erroreinError-Objekt undresultist undefined. - Bei Erfolg ist
errornullundresultenthält den Wert.
Promisification bildet dies direkt ab: Ein nicht-null-Fehler wird zu reject(error), und ein erfolgreiches result wird zu resolve(result).
Eine einzelne Callback-API einwickeln
Hier ist das grundlegende Muster. Wir wickeln eine Callback-basierte Funktion in ein neues Promise ein, rufen reject für den Fehler und resolve für den Wert auf. Das Beispiel simuliert eine Error-First-API mit setTimeout, sodass es überall läuft, auch im Browser:
Der Wrapper nimmt dasselbe id-Argument entgegen, leitet es weiter und stellt seinen eigenen Callback bereit, der in resolve/reject überbrückt. Der Aufrufer sieht keinen Callback mehr.
Den Wrapper mit async/await verwenden
Der eigentliche Vorteil einer Promise-zurückgebenden Funktion ist, dass sie mit async/await funktioniert und asynchronen Code in etwas verwandelt, das von oben nach unten gelesen wird:
Ein generischer promisify()-Helfer
Für jede Funktion von Hand einen Wrapper zu schreiben wird schnell repetitiv. Ein generischer Helfer nimmt eine beliebige Error-First-Funktion entgegen und gibt eine Promise-zurückgebende Version davon zurück. Der Trick besteht darin, alle ursprünglichen Argumente mit einem Rest-Parameter zu sammeln und dann unseren eigenen Callback anzuhängen:
Da der Helfer ...args verwendet und this weiterleitet, funktioniert er für Funktionen mit beliebig vielen führenden Argumenten. In Node.js wird genau das als util.promisify in der Standardbibliothek mitgeliefert, sodass man dort selten einen eigenen schreiben muss:
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
readFile('file.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));Callbacks mit mehreren Argumenten behandeln
Der einfache Helfer geht davon aus, dass der Callback ein einzelnes Ergebnis liefert: callback(err, result). Einige APIs übergeben mehrere Werte, z. B. callback(err, header, body). Ein einfaches resolve(result) würde alles nach dem ersten Wert stillschweigend verwerfen.
Ein Promise kann sich nur mit einem Wert auflösen, also sammelt man die zusätzlichen Argumente in einem array (oder einem object) und löst damit auf:
Node's util.promisify unterstützt dieselbe Idee über ein benutzerdefiniertes Symbol (util.promisify.custom), aber für ad-hoc-Funktionen ist ein array der einfachste Ansatz.
Einschränkungen und Fallstricke
Promisification ist mechanisch, hat aber echte Grenzen:
- Sie erwartet die Error-First-Konvention. Wenn eine Funktion Fehler auf andere Weise signalisiert — z. B. durch einen boolean-Rückgabewert, eine geworfene Ausnahme oder die Reihenfolge
(result, err)— wird ein generischer Helfer sie falsch interpretieren. Solche Funktionen muss man von Hand einwickeln. - Sie behandelt nur eine einzige Fertigstellung. Promises werden einmalig erfüllt. Eine Funktion, die ihren Callback wiederholt aufruft (Events, Streams,
setInterval, ein Fortschritts-Callback), kann nicht promisifiziert werden — nur der erste Aufruf würde das Promise auflösen; spätere Aufrufe werden ignoriert. Für wiederholende Werte verwendet man eine Event-API oder einen Async Iterator. - Ein Promise kann nicht abgebrochen werden. Wenn die zugrunde liegende Callback-API Abbruch unterstützt (z. B. das Löschen eines Timers), geht diese Fähigkeit verloren, sobald sie hinter einem Promise verborgen ist.
- Der Wrapper ändert die Aufruf-Signatur. Aufrufer müssen nun
.then/awaitverwenden, anstatt einen Callback zu übergeben. Man sollte eine Funktion nicht promisifizieren, die noch von anderem Code im Callback-Stil aufgerufen wird, ohne beide Versionen beizubehalten. - Ein
throwinnerhalb des Executors führt weiterhin zur Ablehnung. Code, der synchron innerhalb vonnew Promise((resolve, reject) => { ... })ausgeführt wird, wird abgefangen und in eine Ablehnung umgewandelt — aber ein Fehler, der später innerhalb eines asynchronen Callbacks geworfen wird, wird nicht automatisch abgefangen. Genau deshalb mussreject(err)explizit aufgerufen werden.
Best Practices
- Promisifiziere an der Grenze. Konvertiere I/O- und Timer-APIs einmal, nahe dem Punkt, an dem sie in deinen Code eintreten, und halte den Rest deiner Codebasis Promise-basiert.
- Bevorzuge eingebaute Lösungen. In Node.js greife auf
util.promisify(oder die Modulefs/promises,dns/promisesusw.) zurück, bevor du einen Wrapper von Hand schreibst. - Behandle Ablehnungen immer. Hänge ein
.catch()an oder wickleawaitintry/catchein; eine unbehandelte Ablehnung kann einen Node-Prozess zum Absturz bringen. - Halte Namen vorhersehbar. Eine verbreitete Konvention ist, die Promise-Version mit dem Suffix
Asynczu versehen (readFileAsync), damit beide Stile koexistieren können.
Verwandte Themen
- JavaScript Promise — das Objekt, das du bei der Promisifizierung erstellst.
- Promises Chaining — promisifizierte Aufrufe sauber verketten.
- Async/Await — die Syntax, die promisifizierte Funktionen wie synchronen Code aussehen lässt.
- Callbacks and Beyond — das Muster, von dem du konvertierst.
- Promise API — mehrere promisifizierte Aufrufe mit
Promise.allund Verwandten kombinieren.