Von Callbacks zu Promises

the next big thing  –  11 Kommentare

Die vor einigen Wochen veröffentlichte Version 8.0.0 von Node.js unterstützt die beiden neuen Schlüsselwörter async und await. Sie funktionieren allerdings nur mit Funktionen, die Promises zurückgeben – nicht mit Callbacks. Wie kommt man möglichst einfach aus der einen in die andere Welt?

Bis auf wenige Ausnahmen sind die meisten Funktionen in Node.js asynchron. Allerdings nutzen die Funktionen dafür größtenteils Callbacks. Auf die in der JavaScript-Welt inzwischen etablierten Promises ist das Node.js-Projekt selbst mit seinen integrierten Modulen bislang nicht umgestiegen, und es gibt derzeit auch keine Pläne dafür.

Auf den ersten Blick scheint das egal zu sein, doch auf den zweiten ist es das nicht: In der Version 8.0.0 unterstützt Node.js nämlich die beiden neuen Schlüsselwörter async und await, die den Umgang mit asynchronem Code dramatisch vereinfachen. Mit ihrer Hilfe lässt sich der ständige Wechsel zwischen Callbacks und Promises verhindern, und die Syntax ähnelt synchronem Code.

Allerdings funktionieren die beiden Schlüsselwörter nur mit Funktionen, die auf Promises basieren. Wie also lassen sich beispielsweise die Funktionen aus den integrierten Node.js-Modulen möglichst einfach auf Promises umstellen?

Am offensichtlichsten ist es, die Umstellung von Hand vorzunehmen, indem man für die gewünschte Callback-basierte Funktion eine geeignete Promise-basierte Wrapper-Funktion schreibt:

const fs = require('fs');

const readFile = function (name, options) {
return new Promise((resolve, reject) => {
fs.readFile(name, options, (err, data) => {
if (err) {
return reject(err);
}
resolve(data(;
});
});
};

Das ist zwar prinzipiell nicht schwierig, auf Dauer aber lästig, zumal man die Umstellung für jede Funktion einzeln durchführen muss.

Glücklicherweise folgen die meisten asynchronen Callback-basierten Funktionen wiederum einem bestimmten Schema, das sich in Node.js als Konvention etabliert hat: Der Callback ist stets der letzte Parameter der Funktion. Der zurückgegebene Fehler ist hingegen stets der erste Parameter des Callbacks.

Daher ist es in den meisten Fällen gleich, um welche konkrete Funktion es sich handelt, sodass sich auf einfachem Weg eine promisify-Funktion schreiben lässt:

const promisify = function (fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, function (err, ...result) {
if (err) {
return reject(err);
}
resolve(...result);
});
});
};
};

Nun lässt sich beispielsweise die fs.readFile-Funktion durch einen einfachen Funktionsaufruf verwandeln und anschließend wie gewünscht verwenden:

const readFile = promisify(fs.readFile);

readFile('/etc/passwd', { encoding: 'utf8' }).
then(data => {
console.log(data);
}).
catch(err => {
console.log(err.message);
});

Seit der Version 8.0.0 enthält das util-Modul von Node.js eine solche Hilfsfunktion namens promisify bereits serienmäßig, sodass man sie nicht mehr von Hand schreiben muss. Kombiniert man die Funktion mit den beiden neuen Schlüsselwörtern async und await, lässt sich der Code wie folgt äußerst kompakt und gut lesbar schreiben:

const fs = require('fs'),
util = require('util');

const readFile = util.promisify(fs.readFile);

(async function () {
try {
const data = await readFile('/etc/passwd', { encoding: 'utf8' });

console.log(data);
} catch (ex) {
console.log(ex.message);
}
})();

tl;dr: Node.js 8.0.0 enthält die neue Funktion util.promisify, mit der sich Callback-basierte in Promise-basierte Funktionen umwandeln lassen, die dann mit den neuen Schlüsselwörtern async und await harmonieren.