Einführung in die asynchrone JavaScript-Programmierung

Architektur/Methoden  –  8 Kommentare

Ressourcenzugriffe sind zeitaufwendig. Dank asynchroner Programmierung müssen JavaScript und Node.js aber nicht warten, sondern nutzen die Gelegenheit für das Erledigen anderer Aufgaben.

JavaScript behandelt Funktionen als Bürger erster Klasse. Das bedeutet, dass sich Funktionen und Daten identisch verarbeiten lassen: Programmierer können einer Funktion nicht nur Datenwerte wie Zahlen und Zeichenketten übergeben, sondern auch weitere Funktionen. Das Gleiche gilt für Rückgabewerte.

Die Idee für Funktionen, die ebensolche als Parameter erwarten und zurückgeben, entstammt der funktionalen Programmierung. Dort werden derartige Konstrukte als Funktionen höherer Ordnung bezeichnet.

Ein gängiges Beispiel in JavaScript ist in dem Zusammenhang das Verarbeiten aller Elemente eines Arrays. An die Stelle der klassischen Zählschleife tritt ein Aufruf der forEach-Funktion. Sie erwartet eine Funktion als Parameter, die für jeden einzelnen Wert des Arrays aufgerufen wird:

let primes = [ 2, 3, 5, 7, 11 ];

primes.forEach(prime => {
console.log(prime ** 2);
});

// => 4, 9, 25, 49, 121

Aus technischer Sicht handelt es sich bei dem Lambda-Ausdruck

prime => {
console.log(prime ** 2);
}

um einen Callback. Das Konzept ist nicht neu und auch in anderen Sprachen bekannt, unter anderem in C auf Basis von Funktionszeigern und in C#, wo Delegates zum Einsatz kommen.

Gelegentlich stößt man auf die Behauptung, ein Callback sei ein Zeichen für asynchronen Code. Das ist jedoch nicht wahr: Die forEach-Funktion arbeitet synchron, wie der folgende Code beweist:

primes.forEach(prime => {
console.log(prime ** 2);
});

console.log('done');

// => 4, 9, 25, 49, 121, done

Würde der Callback asynchron aufgerufen, hätte die Ausgabe von done vorzeitig erfolgen müssen, beispielsweise so:

// => 4, done, 9, 25, 49, 121

Dennoch gibt es auch asynchrone Callbacks, wie ein ebenfalls gängiges Beispiel zeigt:

setTimeout(() => {
console.log('World')
}, 10);

console.log('Hello');

// => Hello, World

Obwohl der Aufruf der Funktion setTimeout vor dem Aufruf der Ausgabe von Hello erfolgt, gibt der Code als Ergebnis erst Hello und danach [/i]World[/i] aus.

Asynchrone Callbacks finden in Node.js vor allem dann Verwendung, wenn ein Zugriff auf eine externe Ressource wie das Dateisystem oder das Netzwerk erfolgt:

const http = require('http');

http.get('http://www.thenativeweb.io', res => {
console.log(res.statusCode); // => 200
});

console.log('Requesting...');

Das Programm gibt zunächst die Meldung Requesting... aus, bevor es den Statuscode des Netzwerkzugriffs abrufen kann. Das Beispiel zeigt daher gut den von Node.js propagierten asynchronen, nichtblockierenden Zugriff auf I/O-Ressourcen.

Was auf den ersten Blick wie eine reine Spitzfindigkeit wirkt, erweist sich bei genauerem Hinsehen als echtes Problem. Fehler in asynchronen Callbacks lassen sich von außen nämlich nicht durch try und catch abfangen und behandeln, wie der folgende Codeausschnitt zeigt:

try {
http.get('http://www.thenativeweb.local', res => {
console.log(res.statusCode);
});
} catch (e) {
console.log('Error', e);
}

// => Unhandled 'error' event
// getaddrinfo ENOTFOUND www.thenativeweb.local www.thenativeweb.local:80

Auch das Verschieben von try und catch in den Callback löst das Problem nicht, da ihn das Programm aufgrund der fehlgeschlagenen Namensauflösung niemals aufrufen könnte.

In Node.js gibt es zwei gängige Verfahren, das Problem zu behandeln. Einige APIs lösen ein error-Ereignis aus, die meisten rufen jedoch den Callback auf, allerdings mit einem Error-Objekt als ersten Parameter. Die Funktion http.get folgt dem ersten Ansatz, weshalb der Abruf der Webseite wie folgt zu implementieren ist:

http.get('http://www.thenativeweb.local', res => {
console.log(res.statusCode);
}).on('error', err => {
console.log('Error', err);
});

// => Error { [Error: getaddrinfo ENOTFOUND www.thenativeweb.local www.thenativeweb.local:80]
// code: 'ENOTFOUND',
// errno: 'ENOTFOUND',
// syscall: 'getaddrinfo',
// hostname: 'www.thenativeweb.local',
// host: 'www.thenativeweb.local',
// port: 80 }

Wesentlich häufiger trifft man allerdings auf Callbacks, die als ersten Parameter einen potenziellen Fehler und als zweiten die eigentlichen Daten erwarten. Ein Beispiel hierfür ist die fs.readFile-Funktion, mit der sich eine Datei vom Dateisystem laden und einlesen lässt:

const fs = require('fs');

fs.readFile('/etc/passwd', (err, data) => {
if (err) {
return console.log('Error', err);
}
console.log(data.toString('utf8'));
});

Wichtig ist es, bei derartigen Funktionen darauf zu achten, den err-Parameter tatsächlich abzufragen und angemessen zu reagieren. Eine fehlende if-Abfrage führt rasch dazu, dass ein aufgetretener Fehler verschluckt wird, was selten dem gewünschten Verhalten entspricht.

Werkzeuge zur Codeanalyse wie ESLint verfügen daher häufig über Regeln, die prüfen, ob das Programm den err-Parameter abfragt. Im Beispiel von ESLint implementiert die Regel handle-callback-err den entsprechenden Mechanismus.

Außerdem ist darauf zu achten, das weitere Ausführen der Funktion im Fehlerfall abzubrechen – beispielsweise mit einer return-Anweisung.

Entscheidend für die Konsistenz und Verlässlichkeit einer API ist zudem, dass sich eine Funktion stets gleichartig verhält: Wenn sie einen Callback entgegennimmt, sollte er entweder immer synchron oder immer asynchron aufgerufen werden, aber nicht von Fall zu Fall wechseln.

Der zuvor erwähnte Blog-Eintrag fasst das in der einfachen Regel "Choose sync or async, but not both" zusammen und begründet sie wie folgt:

"Because sync and async callbacks have different rules, they create different bugs. It's very typical that the test suite only triggers the callback asynchronously, but then some less-common case in production runs it synchronously and breaks. (Or vice versa.) Requiring application developers to plan for and test both sync and async cases is just too hard, and it's simple to solve in the library: If the callback must be deferred in any situation, always defer it."

Auch Isaac Z. Schlueter, der Autor von npm, warnt in seinem Blog-Eintrag "Designing APIs for Asynchrony" davor, APIs so zu gestalten, dass ihr Verhalten in Bezug auf synchrone beziehungsweise asynchrone Ausführung nicht deterministisch ist.

Um dem Problem zu begegnen, existieren zwei Funktionen, die auf den ersten Blick austauschbar zu sein scheinen: process.nextTick und setImmediate. Beide erwarten einen Callback als Parameter und führen ihn zu einem späteren Zeitpunkt aus. Daher scheinen der Aufruf von

process.nextTick(() => {
// Do something...
});

und der von

setImmediate(() => {
// Do something...
});

äquivalent zu sein. Intern unterscheiden sich die beiden Varianten allerdings: process.nextTick verzögert das Ausführen des Callbacks auf einen späteren Zeitpunkt, führt ihn aber aus, bevor I/O-Zugriffe erfolgen und die Eventloop wieder die Kontrolle übernimmt.

Daher können rekursive Aufrufe der Funktion dazu führen, die Übergabe immer weiter hinauszuzögern und die Event Loop effektiv "verhungern" zu lassen. Den Effekt bezeichnet man dementsprechend als "Event Loop Starvation".

Die Funktion setImmediate umgeht das Problem, indem sie das Ausführen des Callbacks auf die nächste Iteration der Ereignisschleife verschiebt. Der Blog-Eintrag zur Veröffentlichung von Node.js 0.10 beschreibt die Unterschiede zwischen den beiden Funktionen in detaillierter Form.

In der Regel genügt allerdings process.nextTick, um einen Callback asynchron statt synchron aufzurufen. Auf dem Weg lässt sich Code, der auf beide Weisen arbeitet, generell asynchron ausführen:

let load = function (filename, callback) {
load.cache = load.cache || {};

let data = load.cache[filename];

if (data) {
return callback(null, data.toString('utf8')); // Synchronous
}

fs.readFile(filename, (err, data) => {
if (err) {
return callback(err);
}

load.cache[filename] = data;
callback(null, data.toString('utf8')); // Asynchronous
});
};

Bringt man den Code mit process.nextTick in eine vollständig asynchrone Form, ist die Verwendung konsistent und verlässlich möglich. Allerdings gilt es, darauf zu achten, die Position der return-Anweisung dem neuen Ablauf anzupassen:

let load = function (filename, callback) {
load.cache = load.cache ||Â {};

let data = load.cache[filename];

if (data) {
return process.nextTick(() => {
callback(null, data.toString('utf8')); // Now asynchronous as well
});
}

fs.readFile(filename, (err, data) => {
if (err) {
return callback(err);
}

load.cache[filename] = data;
callback(null, data.toString('utf8')); // Asynchronous
});
};

Vergleicht man die asynchrone Implementierung der load-Funktion mit der synchronen Variante fällt auf, dass der synchrone Code kürzer und besser verständlich ist. Außerdem entfällt das Risiko, einen Fehler zu verschlucken. Schlägt synchroner Code fehl, wird eine Ausnahme ausgelöst, die unbehandelt zum Abbruch des Prozesses führt.

let load = function (filename) {
load.cache = load.cache ||Â {};

let data = load.cache[filename];
if (data) {
return data.toString('utf8');
}

data = fs.readFileSync(filename);
load.cache[filename] = data;

return data.toString('utf8');
};

Synchroner Code erlaubt außerdem den Einsatz der klassischen Mittel zur Ablaufsteuerung wie for-Schleifen oder try-catch-Blöcke. Der einzige Nachteil besteht im Stillstand, während auf eine externe Ressource gewartet wird. Die Dokumentation von Node.js empfiehlt daher, den Einsatz synchroner Funktionen zu vermeiden, wenn ein asynchrones Pendant zur Verfügung steht:

"In busy processes, the programmer is strongly encouraged to use the asynchronous versions of these calls. The synchronous versions will block the entire process until they complete – halting all connections."

Die Entscheidung zwischen synchronem und asynchronem Code ist also letztlich ein Abwägen zwischen guter Lesbarkeit auf der einen und performanter Ausführung auf der anderen Seite. Wünschenswert wäre, beides zu vereinen.

Ein verhältnismäßig weitverbreiteter Ansatz ist der Einsatz von Promises. Dabei handelt es sich um spezielle Objekte, die eine Funktion synchron zurückgeben kann, deren Wert das Programm aber erst zu einem späteren Zeitpunkt festlegt.

ECMAScript 2015 (ehemals ECMAScript 6 "Harmony") enthält den Promise-Konstruktor serienmäßig, weshalb der Einsatz eines Polyfills inzwischen nicht mehr zwingend erforderlich ist. Die große Ausnahme ist leider wieder einmal der Internet Explorer.

Um ein Promise zu erzeugen, muss man den Konstruktor aufrufen und einen Callback übergeben, der seinerseits zwei Funktionen entgegennimmt: resolve und reject. Sie sind einzusetzen, um das Versprechen einzulösen beziehungsweise es im Fehlerfall zu brechen:

return new Promise((resolve, reject) => {
// ...
});

Schreibt man die bisher asynchrone load-Funktion auf den Einsatz eines Promises um, entsteht der folgende Code. Primär unterscheidet er sich von der asynchronen Variante lediglich durch das Fehlen des Callbacks:

let load = function (filename) {
load.cache = load.cache || {};

return Promise((resolve, reject) => {
let data = load.cache[filename];

if (data) {
return process.nextTick(() => {
resolve(data.toString('utf8'));
});
}

fs.readFile(filename, (err, data) => {
if (err) {
return reject(err);
}

load.cache[filename] = data;
resolve(data.toString('utf8'));
});
});
};

Ruft man die Funktion load auf, erhält man ein Promise zurück, das wiederum Funktionen wie then und catch bietet, um die zurückgegebenen Daten beziehungsweise einen aufgetretenen Fehler zu verarbeiten:

load('/etc/passwd').then(data => {
// ...
}).catch(err => {
// ...
});

Da asynchrone Funktionen in Node.js stets dem Schema folgen, Parametern zunächst einen Fehler und erst danach die eigentlichen Daten zu übergeben, lässt sich leicht eine Funktion promisify schreiben, die eine beliebige mit Callbacks arbeitende Funktion in eine überführt, die Promises nutzt:

let promisify = function (obj, fn) {
return function (...args) {
return new Promise((resolve, reject) => {
obj[fn].apply(obj, [...args, (err, ...result) => {
if (err) {
return reject(err);
}
resolve(...result);
}]);
});
};
};

Um nun eine Callback nutzende Funktion auf Grundlage eines Promise zu verwenden, ist sie einmalig mit promisify in eine entsprechende Funktion zu verpacken:

let fsReadFile = promisify(fs, 'readFile');

fsReadFile('/etc/passwd').then(data => {
// ...
}).catch(err => {
// ...
});

Da sich Promises aneinander hängen lassen, können Ketten aus then-Funktionen entstehen, an deren Ende ein einziger Aufruf von catch genügt, um Fehler zu behandeln. Das löst zwar das Problem der sogenannten Callback Hell, macht den asynchronen Code allerdings nicht lesbarer.

Darüber hinaus lassen sich die klassischen Konstrukte zur Ablaufsteuerung nach wie vor nicht einsetzen und Fehler können weiterhin untergehen, sollten die Entwickler den Aufruf von catch vergessen haben.

Daher lässt sich das ursprüngliche Ziel, den Code kürzer und besser lesbar zu machen, kaum als erreicht ansehen.

Neben Promises enthält ES2015 zwei weitere neue Sprachmerkmale, die im Kontext der asynchronen Programmierung interessant sind. Gemeint sind zum einen sogenannte Generatorfunktionen, zum anderen das Schlüsselwort yield.

Die Idee hinter Letzterem ist, das Ausführen einer Funktion zu unterbrechen, um einen bereits berechneten Wert aus einer ganzen Reihe zu berechnender Werte vorzeitig zurückgeben zu können. Als Beispiel sei die Berechnung von Primzahlen genannt, da die Aufgabe für große Zahlen zeitaufwendig ist:

let isPrimeFactor = function (factor, number) {
return number % factor === 0;
};

let isPrime = function (candidate) {
if (candidate < 2) {
return false;
}

for (let factor = 2; factor <= Math.sqrt(candidate); factor++) {
if (isPrimeFactor(factor, candidate)) {
return false;
}
}

return true;
};

let getPrimes = function (min, max) {
let primes = [];

for (let candidate = min; candidate <= max; candidate++) {
if (isPrime(candidate)) {
primes.push(candidate);
}
}

return primes;
}

Ruft man die Funktion getPrimes mit kleinen Zahlen und einem kleinen Intervall auf, gibt sie das gewünschte Ergebnis rasch zurück:

let primes = getPrimes(1, 20);
// => [ 2, 3, 5, 7, 11, 13, 17, 19 ]

Für größere Werte und Intervalle rechnet die Funktion jedoch abhängig von den gewählten Zahlen durchaus einige Sekunden. Es wäre hilfreich, bereits berechnete Primzahlen ausgeben zu können, während die Berechnung der übrigen noch läuft.

Genau das ermöglicht das Schlüsselwort yield. Es verhält sich prinzipiell wie die return-Anweisung, speichert aber den Zustand der Funktion, sodass sie sich zu einem späteren Zeitpunkt fortsetzen lässt. Allerdings ist es nicht möglich, das Schlüsselwort in jeder beliebigen Funktion zu verwenden, sondern nur in Generatorfunktionen. Sie werden in JavaScript mit function * statt function definiert:

let getPrimes = function * (min, max) {
for (let candidate = min; candidate <= max; candidate++) {
if (isPrime(candidate)) {
yield candidate;
}
}
}

Ruft man eine Generatorfunktion auf, führt sie im Gegensatz zu einer normalen Funktion nicht den in ihr enthaltenen Code aus, sondern gibt zunächst ein Iteratorobjekt zurück. Auf ihm lässt sich dann die Funktion next aufrufen, um die eigentliche Funktion auszuführen – allerdings nur bis zum ersten Aufruf von yield:

let iterator = getPrimes(1, 10);

console.log(iterator.next());
// => { value: 2, done: false }

Ruft man die next-Funktion erneut auf, setzt das Programm die Ausführung der Funktion fort, bis es auf ein weiteres yield oder das Ende des auszuführenden Codes stößt:

let iterator = getPrimes(1, 10);

console.log(iterator.next()); // => { value: 2, done: false }
console.log(iterator.next()); // => { value: 3, done: false }
console.log(iterator.next()); // => { value: 5, done: false }
console.log(iterator.next()); // => { value: 7, done: false }
console.log(iterator.next()); // => { value: undefined, done: true }

Um den Umgang mit Iteratoren zu vereinfachen, kennt ES2015 die for-of-Schleife, die einen Iterator erzeugt und durchläuft:

for (let prime of getPrimes(1, 10)) {
console.log(prime);
}
// => 2, 3, 5, 7

Besonders interessant ist, dass man der next-Funktion wiederum Parameter übergeben kann, die in der getPrimes-Funktion als Rückgabewert von yield zur Verfügung stehen. Damit lässt sich beispielsweise eine Schleife zur Berechnung unendlich vieler Primzahlen schreiben, die von außen abgebrochen werden kann:

let getPrimesFrom = function * (min) {
for (let candidate = min; ; candidate++) {
if (isPrime(candidate)) {
let shallContinue = yield candidate;

if (!shallContinue) {
return;
}
}
}
}

Nun lässt sich beispielsweise die Verarbeitung beenden, sobald fünf Primzahlen berechnet wurden. Der erste Aufruf von next nimmt dabei noch keinen Parameter entgegen, da er das Ausführen der Funktion überhaupt erst startet und daher noch kein yield erreicht wurde, dem sich ein Rückgabewert übergeben ließe:

let primesIterator = getPrimesFrom(1);
console.log(primesIterator.next()); // => { value: 2, done: false }
console.log(primesIterator.next(true)); // => { value: 3, done: false }
console.log(primesIterator.next(true)); // => { value: 5, done: false }
console.log(primesIterator.next(true)); // => { value: 7, done: false }
console.log(primesIterator.next(true)); // => { value: 11, done: false }
console.log(primesIterator.next(false)); // => { value: undefined, done: true }

Betrachtet man die Zeile

let shallContinue = yield candidate;

isoliert, fällt auf, dass das Zurückgeben der Variablen candidate und das Entgegennehmen des Rückgabewerts zeitlich getrennt stattfinden: Der externe Aufruf von next bestimmt, wie viel Zeit dazwischen vergeht. Letztlich entspricht das also dem Pausieren einer Funktion, wobei sich während der Wartezeit anderer Code ausführen lässt.

Wenn das gleiche Vorgehen auf asynchronen Code anwendbar wäre, ließe sich ein asynchroner Aufruf wie folgt schreiben:

let data = yield fs.readFile('/etc/passwd');

Die Möglichkeit würde die Lesbarkeit von asynchronem Code deutlich verbessern, da der einzige Unterschied zwischen einem asynchronen und einem synchronen Aufruf in der Verwendung des Schlüsselworts yield bestünde.

Allerdings müsste die Funktion fs.readFile dann so geschrieben sein, dass sie keinen Callback erwartet, sondern ein Objekt synchron zurückgibt, auf das an anderer Stelle gewartet und reagiert werden kann. Genau das ermöglichen Promises:

let fsReadFile = promisify(fs, 'readFile');
let data = yield fsReadFile('/etc/passwd');

Das Beispiel funktioniert dennoch nicht, da noch eine Ablaufsteuerung fehlt, die auf das Promise reagiert und intern next aufruft. Das leistet das Modul co.

Der Einsatz von co erübrigt sich allerdings in absehbare Zeit, da die nächste Version von JavaScript, ES7, eingebaute Unterstützung für das Vorgehen mit den Schlüsselwörtern async und await enthält. Das Schlüsselwort async tritt dann an die Stelle der Generatorfunktionen, await ersetzt yield.

Wäre ES7 bereits heute verfügbar, ließe sich die load-Funktion wie folgt schreiben:

let fsReadFile = promisify(fs, 'readFile');

let load = async function (filename) {
load.cache = load.cache || {};

let data = load.cache[filename];
if (data) {
return data.toString('utf8');
}

data = await fsReadFile(filename);
load.cache[filename] = data;

return data.toString('utf8');
};

Das entspricht mit Ausnahme der beiden neuen Schlüsselwörter exakt dem synchronen Code. Auf die Weise verbessert sich nicht nur die Lesbarkeit deutlich, sondern Entwickler können auch der Callback Hell entgehen.

Außerdem ist es nicht mehr möglich, Fehler versehentlich zu verschlucken, da async und await dafür Sorge tragen, dass im Fall eines zurückgewiesenen Promises eine Ausnahme geworfen wird, die mit try und catch abzufangen ist. Zu guter letzt lassen sich auch die übrigen Konstrukte zur Ablaufsteuerung verwenden, beispielsweise for-Schleifen.

Der einzige Haken ist, dass sich await ausschließlich in Funktionen verwenden lässt, die als async markiert sind. Das bedeutet, dass auf oberster Ebene eine async-Funktion stehen muss. Das lässt sich jedoch einfach bewerkstelligen, indem ein asynchroner Lambda-Ausdruck als "Main"-Funktion zum Einsatz kommt, der automatisch ausgeführt wird:

(async () => {
let data = await load('/etc/passwd');
console.log(data);
})();

Obwohl sich der Code wie synchroner Code liest, verhält er sich asynchron: Node.js blockiert nicht, während es wartet, dass die Datei fertig geladen ist. Unter der Haube arbeitet er nach wie vor mit Promises und Callbacks, für Entwickler verbirgt die Syntax das jedoch auf elegante Weise.

Das bedeutet allerdings, dass eine nicht behandelte Ausnahme abzufangen ist, da auch sie asynchron auftritt. Daher ist es ratsam, auf oberster Ebene ein globales try zu verwenden:

(async () => {
try {
let data = await load('/etc/passwd');
console.log(data);
} catch (err) {
console.error(err);
}
})();

Alternativ lässt sich auf das Ereignis process.unhandledRejection reagieren:

process.on('unhandledRejection', (reason, p) => {
console.error(`Unhandled Rejection at: Promise ${p}, reason: ${reason}`);
});

(async () => {
let data = await load('/etc/passwd');
console.log(data);
})();

Besonders interessant ist die Fähigkeit von await, auf mehrere asynchrone Funktionen gleichzeitig warten zu können, die parallel ausgeführt werden. Dazu dient das alternative Schlüsselwort await*, das im zugehörigen Proposal beschrieben ist.

Auch wenn die neuen Schlüsselwörter in ES7 noch nicht final sind und ES7 davon abgesehen auch noch nicht auf breiter Front verfügbar ist, lässt sich die neue Syntax dennoch verwenden. Das Projekt Babel ermöglicht das, indem es einen Compiler anbietet, der zukünftig lauffähigen ES2015- und ES7-Code in heute ausführbaren ES5-Code übersetzt.

Im einfachsten Fall installiert man Babel global via npm:

npm install -g babel

Dabei verwendet der Compiler die lokal vorhandene Version von Node.js als Ausführungsumgebung. Da die Sprachmerkmale von ES7 noch als experimentell eingestuft sind, ist die Unterstützung für sie beim Aufruf von Babel explizit zu aktivieren:

babel-node --optional es7.asyncFunctions app.js

Alternativ lässt sich Babel auch auf anderen Wegen installieren. Die Dokumentation beschreibt die unterschiedlichen Vorgehensweisen.

Die Unsicherheit der Vergangenheit, welchen Ansatz man für die asynchrone Programmierung verwenden sollte, geht langsam zu Ende: JavaScript unterstützt Promises und wird in der kommenden Version ES7 die beiden neuen Schlüsselwörter async und await enthalten, die den Umgang mit Promises durch eine elegante Syntax vereinfachen.

Es gibt folglich keinen Grund mehr, neuen APIs keine Promises zugrunde zu legen, um für die Zukunft gerüstet zu sein. Da Promises als Bestandteil von ES2015 mit Ausnahme des Internet Explorer durchgängig verfügbar sind, ist hierfür in vielen Fällen noch nicht einmal ein Polyfill erforderlich.

Da die Schlüsselwörter async und await syntaktisch stark an ihre Vorbilder in C# angelehnt sind, ist davon auszugehen, dass sich an ihrer Syntax nicht mehr viel verändert. Daher spricht nichts dagegen, sie in Verbindung mit Babel zu verwenden, zumal sich das Projekt ohnehin mehr und mehr als De-facto-Standard im Bereich der JavaScript-Compiler etabliert.

Die größte Herausforderung bei all dem ist, das riesige Ökosystem von JavaScript und Node.js nach und nach zu portieren. Der Einsatz von Funktionen wie promisify ist dauerhaft nur als Workaround anzusehen und sollte langfristig vermieden werden. Bis dahin leistet die Funktion allerdings gute Dienste, um eine Brücke zwischen alter und neuer Welt zu schlagen. (jul)

Golo Roden
ist Gründer der "the native web UG", eines auf native Webtechniken spezialisierten Unternehmens. Für die Entwicklung moderner Webanwendungen bevorzugt er JavaScript und Node.js und hat mit "Node.js & Co." das erste deutschsprachige Buch zum Thema geschrieben.