async und await für Node.js

the next big thing  –  3 Kommentare
Anzeige

Gestern wurde Node.js in der Version 7.6 veröffentlicht. Was zunächst lediglich nach einem unbedeutenden Versionssprung klingt, ist tatsächlich ein bedeutender Schritt für jeden JavaScript-Entwickler. Die neue Version enthält nämlich die beiden aus C# bekannten Schlüsselwörter async und await. Wie funktionieren sie?

Das Changelog von Node.js 7.6 wirkt unspektakulär. Ein paar Änderungen hier, ein paar Änderungen dort, und ein Update des zugrunde liegenden JavaScript-Compilers V8 auf die neue Version 5.5. Doch dieser Punkt hat es in sich, denn V8 5.5 unterstützt die beiden neuen Schlüsselwörter async und await.

Anzeige

Wer bei async an asynchrone Programmierung denkt, liegt richtig. Dank einer leichten, aber leistungsfähigen Eventloop, nichtblockierender I/O und Funktionen als Datentyp erster Klasse ist die asynchrone Programmierung seit jeher eine Disziplin, in der sich Node.js aus technischer Sicht hervorragend schlägt. Leider geht das jedoch häufig mit syntaktischen Schwierigkeiten einher.

Das liegt daran, dass die asynchrone Programmierung in Node.js letztlich auf Callbacks beruht, die ineinander verschachtelt werden. Das führt rasch zu unlesbarem und schlecht nachvollziehbarem Code, wenn ein Callback den nächsten enthält. Das Problem ist in der Node.js-Szene als Callback-Hölle bekannt.

Ansätze zum Lösen des Problems gibt es viele, doch hat sich keiner in der Vergangenheit als das Mittel der Wahl durchsetzen können. Alle bringen ihre eigenen neuen Probleme mit, seien es Promises, Generatorfunktionen oder Module wie async.js.

Seit gestern gibt es nun einen neuen Ansatz, der auf den beiden neuen Schlüsselwörtern async und await basiert. C#-Entwicklern dürften sie bekannt vorkommen, und tatsächlich handelt es sich um das gleiche Konzept, das sogar ziemlich ähnlich nun auch in JavaScript umgesetzt wurde.

Musste man eine Funktion, die ein Promise zurückgibt, bisher aufrufen, um dann mit then und catch weiterzuarbeiten, lässt sich nun darauf warten. Dazu dient das Schlüsselwort await. An die Stelle von

sleep(1000).
then(() => {
console.log('Hallo Welt!');
}).
catch(err => {
// ...
});

tritt nun der folgende Code:

try {
await sleep(1000);
console.log('Hallo Welt!');
} catch (ex) {
// ...
}

Er liest sich synchron, wird intern aber asynchron ausgeführt. Das await wartet also nicht in dem Sinne, dass der ausführende Thread blockiert. Stattdessen wird der Aufruf vom Compiler in eine Koroutine zerlegt, sodass während des Wartens anderer asynchroner Code laufen kann.

Das Schlüsselwort await lässt sich allerdings nicht überall verwenden, sondern nur in speziellen als asynchron gekennzeichneten Funktionen. Dazu dient das Schlüsselwort async. Will man den vorigen Code also ausführen, gilt es, ihn entsprechend zu verpacken:

const sayHello = async function () {
try {
await sleep(1000);
console.log('Hallo Welt!');
} catch (ex) {
// ...
}
};

Auf diese Funktion kann der Aufrufer wiederum mit await warten. Dazu muss es sich allerdings ebenfalls um eine asynchrone Funktion handeln, auf die sich wieder warten lässt, wenn der Aufrufer eine asynchrone Funktion ist, auf den …

Diese Kette wirft die Frage auf, was am Anfang steht. Müsste dann nicht die main-Funktion eines Programms bereits asynchron sein? Tatsächlich ist diese Frage zunächst zu bejahen. Die Realität sieht aber anders aus, denn main ist in Node.js nach wie vor synchron.

Abhilfe schafft die moderne Form einer sich selbst ausführenden Funktion, wie sie in JavaScript seit Jahren bekannt ist. Der einzige Unterschied zu bisher ist das async-Schlüsselwort:

(async () => {
await sayHello();
})();

Wie das Beispiel zeigt, funktioniert async auch mit Lambda-Ausdrücken.

Am anderen Ende der Kette steht, wie bereits erwähnt, eine Funktion, die ein Promise zurückgibt. Löst das Promise zudem einen Wert auf, erhält man diesen als Rückgabewert von await:

const body = await request('https://www.thenativeweb.io');

Das bedeutet, dass man gut beraten ist, grundlegende APIs nun nach und nach von Callbacks auf Promises umzustellen, um in den Schichten darüber mit async und await arbeiten zu können. Wie einfach das ist, zeigt das folgende Beispiel, in dem die zuvor verwendete sleep-Funktion implementiert wird:

const sleep = function (ms) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, ms);
});
};

Auch die request-Funktion lässt sich leicht implementieren:

const needle = require('needle');

const request = function (url) {
return new Promise((resolve, reject) => {
needle.get(url, (err, res) => {
if (err) {
return reject(err);
}
if (res.statusCode !== 200) {
return reject(new Error('Unexpected status code.'));
}

resolve(res.body);
});
});
};

Wie die vorigen Beispiele zeigen, lassen sich async und await mit klassischen Kontrollstrukturen verwenden, beispielsweise try und catch. Das wäre sowohl bei Callbacks als auch bei reinen Promises undenkbar gewesen. Die neuen Schlüsselwörter lassen asynchronen Code aber synchron aussehen. Das schließt das Verwenden der synchronen Kontrollstrukturen mit ein.

Was bei der Fehlerbehandlung ganz nett ist, entfaltet sein volles Potenzial bei Schleifen, denn auch diese lassen sich nun in Verbindung mit async und await nutzen. Sie funktionieren dabei genau so, wie man es intuitiv erwarten würde. Wer schon einmal versucht hat, eine asynchrone for-Schleife in rein Callback- oder Promise-gestütztem Code zu schreiben, weiß, wie umständlich das bislang war.

Kritiker mögen einwenden, dass async und await kein grundlegendes Problem der Entwicklung lösen. Node.js bekommt dadurch nicht mehr Fähigkeiten, und es lassen sich keine neuen fachlichen Probleme dadurch lösen. Wer die beiden Schlüsselwörter derart betrachtet, verkennt aber, dass auch die Produktivität und vor allem die Lesbarkeit von Code wichtige Ziele von Softwareentwicklung sind.

Wem es gelingt, Entwicklern das Leben bedeutend leichter zu machen, die Nachvollziehbarkeit und damit die Wartbarkeit von Code deutlich zu steigern, und gleichzeitig die Fehleranfälligkeit zu senken, der bewirkt unter Umständen für die Gesamtheit der modernen Softwareentwicklung mehr als derjenige, der eine neue API schafft, die ein bisher unbeachtetes Problem adressiert.

So gesehen sind die beiden unscheinbaren Wörter async und await einer der größten Umbrüche in der Art, wie JavaScript-Code geschrieben wird, und sie erleichtern das tagtägliche Leben von Millionen von Entwicklern. Fangen wir an, sie zu verwenden – besser heute als morgen.

tl;dr: Node.js 7.6 enthält nun V8 in der Version 5.5, die Unterstützung für async und await bereitstellt. Damit lässt sich asynchroner Code schreiben, der sich synchron liest und der synchrone Kontrollstrukturen verwenden kann, aber dennoch asynchron ausgeführt wird.

Anzeige