Resource accesses are time consuming. But asynchronous programming does not require JavaScript and Node.js to wait, but takes the opportunity to do other things.

JavaScript handles functions as a first-class citizen. This means that functions and data can be processed identically: programmers can not only pass data values such as numbers and strings to a function, but also other functions. The same applies to return values.

The idea for functions that expect and return as parameters comes from functional programming. There, such constructs are referred to as higher order functions.

A common example in JavaScript is processing all elements of an array. The classical counting loop is replaced by a call to the forEach function. It expects a function as a parameter that is called for each value of the array:

let primes = [ 2, 3, 5, 7, 11 ]; primes.forEach(prime => { console.log(prime * 2); }); // => 4, 9, 25, 49, 121 

From a technical point of view, it is the lambda expression


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

For a callback. The concept is not new and is also known in other languages, including C based on function pointers and C # where delegates are used.

Occasionally one encounters the claim that a callback is a sign of asynchronous code. However, this is not true: The forEach function works synchronously, as the following code proves:

primes.forEach(prime => { console.log(prime ** 2); }); console.log('done'); // => 4, 9, 25, 49, 121, done 

If the callback was called asynchronously, the output of done should have been premature, for example:


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

Synchronous and asynchronous callbacks


Nevertheless, there are also asynchronous callbacks, as an equally common example shows:

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

Although the call to the setTimeout function occurs before calling the output of Hello, the code outputs Hello first, then [/ i] World [/ i].

Asynchronous callbacks are most commonly used in Node.js when accessing an external resource such as the file system or network:

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

The program first issues the message Requesting … before it can retrieve the status code of the network access. The example therefore shows well the asynchronous, non-blocking access to I / O resources propagated by Node.js.

What looks at first glance like a pure sophistry, on closer inspection turns out to be a real problem. Namely, errors in asynchronous callbacks can not be intercepted and handled externally by try and catch, as the following code snippet shows:

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 

Moving try and catch into the callback does not solve the problem because the program could never call it because of the failed name resolution.


Solution approaches in comparison


In Node.js, there are two common ways to handle the problem. Some APIs trigger an error event, but most call callback, but with an Error object as the first parameter. The function http.get follows the first approach, which is why the retrieval of the web page is to be implemented as follows:

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 } 

Much more frequently, however, one encounters callbacks that expect a potential error as the first parameter and the actual data as the second parameter. An example of this is the fs.readFile function, which allows a file to be loaded and read by the file system:


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

It is important to pay attention to such functions to actually query the err parameter and to respond appropriately. A missing if query quickly results in an error being swallowed, which rarely matches the desired behavior.

Code analysis tools such as ESLint often have rules that check that the program is querying the err parameter. In the example of ESLint, the rule handle-callback-err implements the appropriate mechanism.

In addition, care must be taken to stop further execution of the function in the event of an error – for example with a return statement.

Consistency, advantages and disadvantages


Consistent asynchronous APIs


Another decisive factor for the consistency and reliability of an API is that a function always behaves in a similar way: when it accepts a callback, it should either always be called synchronously or always asynchronously, but not switch from case to case.

The previously mentioned blog entry summarizes this in the simple rule “Choose sync or async, but not both” and justifies it as follows:

“Because sync and async callbacks have different rules, they create different bugs.” (Or vice versa.) Requiring application developers to plan 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. “

Isaac Z. Schlueter, the author of npm, warns in his blog entry “Designing APIs for Asynchrony” that APIs should be designed so that their behavior with respect to synchronous or asynchronous execution is not deterministic.

To address the problem, there are two functions that seem to be interchangeable at first glance: process.nextTick and setImmediate. Both expect a callback as a parameter and execute it at a later time. Therefore, the call from

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

and the of


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

To be equivalent. Internally, however, the two variants differ: process.nextTick delays the execution of the callback to a later date, but executes it before I / O accesses occur and the Eventloop takes over control again.

Therefore, recursive calls to the function may cause the handover to be delayed further and effectively “starve” the event loop. Accordingly, the effect is called “Event Loop Starvation”.

The setImmediate function overcomes the problem by moving the callback to the next iteration of the event loop. The blog entry for the release of Node.js 0.10 describes the differences between the two functions in more detail.

Usually, however, process.nextTick is sufficient to call a callback asynchronously instead of synchronously. Along the way, code that works both ways is generally asynchronous:

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 }); }; 

Bringing the code with process.nextTick in a completely asynchronous form, the use is consistent and reliable possible. However, it is important to remember to adjust the position of the return statement to the new procedure:

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 }); }; 

Advantages and disadvantages of synchronous and asynchronous code


If you compare the asynchronous implementation of the load function with the synchronous variant, you will notice that the synchronous code is shorter and easier to understand. In addition, there is no risk of swallowing a mistake. If synchronous code fails, an exception is thrown which, if left untreated, causes the process to be aborted.

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'); }; 

Synchronous code also allows the use of classical flow control tools such as for loops or try-catch blocks. The only drawback is at a standstill while waiting for an external resource. The Node.js documentation therefore recommends avoiding the use of synchronous functions when an asynchronous counterpart is available:

“In busy processes, the programmer is forced to use the asynchronous versions of these calls.”

The decision between synchronous and asynchronous code is thus ultimately a balance between good readability on the one hand and high-performance execution on the other hand. It would be desirable to combine both.

Promises, yield


Approach with Promises


A relatively common approach is the use of promises. These are special objects that can return a function synchronously, but whose value is set by the program at a later time.

ECMAScript 2015 (formerly ECMAScript 6 “Harmony”) contains the Promise constructor as standard, which is why the use of a polyfill is no longer mandatory. Unfortunately, the big exception is once again Internet Explorer.

To create a promise, one must call the constructor and pass a callback, which in turn takes two functions: resolve and reject. They are to be used to fulfill the promise or to break it in case of error:

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

Writing the asynchronous load function to the use of a promise results in the following code. Primary, it differs from the asynchronous variant only by the lack of callback:


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')); }); }); }; 

Calling the load function returns a Promise, which in turn provides functions such as then and catch to handle the returned data or error:


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

Since asynchronous functions in Node.js always follow the scheme of first passing an error to parameters and then passing the actual data, it is easy to write a promisify function that transforms any callback-based function into one that Promises uses:


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); }]); }); }; }; 

In order to use a callback-using function based on a promise, it has to be packed once with promisify into a corresponding function:


let fsReadFile = promisify(fs, 'readFile'); fsReadFile('/etc/passwd').then(data => { // ... }).catch(err => { // ... }); 

Because promises can cling to each other, chains can arise from then functions, and at the end, a single call to catch is enough to handle errors. Although this solves the problem of so-called Callback Hell, makes the asynchronous code, however, unreadable.

In addition, the classical flow control constructs still can not be used, and bugs may continue to go down if the developers forget to call catch.

Therefore, the original goal of making the code shorter and more readable is hard to come by.

Generator functions and yield


In addition to Promises, ES2015 includes two additional new language features that are of interest in the context of asynchronous programming. This refers to so-called generator functions, on the other hand the keyword yield.

The idea behind the latter is to interrupt the execution of a function in order to be able to prematurely return an already calculated value from a whole series of values to be calculated. An example is the calculation of prime numbers, because the task is time consuming for large numbers:

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; } 

Calling the getPrimes function with small numbers and a small interval will quickly return the desired result:


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

For larger values and intervals, however, the function calculates a few seconds depending on the selected numbers. It would be helpful to be able to output primes that have already been calculated while the others are still running.


yield in action


This is exactly what the keyword yield allows. In principle, it behaves like the return statement, but stores the state of the function so that it can be continued at a later time. However, it is not possible to use the keyword in any function, but only in generator functions. They are defined in JavaScript with function * instead of function:

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

When a generator function is called, unlike a normal function, it does not execute the code contained in it, but first returns an iterator object. It then calls the next function to do the actual function, but only until the first call to yield:


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

When the next function is called again, the program continues to execute the function until it encounters another yield or the end of the code to be executed:


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 } 

To simplify the handling of iterators, ES2015 knows the for-of-loop that generates and iterates through an iterator:


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

Of particular interest is that you can pass parameters to the next function, which are available in the getPrimes function as the return value of yield. This can be used, for example, to write a loop for the calculation of infinitely many primes, which can be aborted from the outside:


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

For example, processing can be stopped as soon as five primes have been calculated. The first call to next does not yet accept a parameter because it only starts the execution of the function, and therefore no yield has yet been reached, to which a return value could be passed:


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 } 

Generator functions, async and await


Generator functions for asynchronous programming


Looking at the line

let shallContinue = yield candidate; 

is isolated, it is noticeable that the return of the variable candidate and the acceptance of the return value take place separately: The external call of next determines how much time elapses in between. In the end, this is equivalent to pausing a function, which can be executed while waiting for other code.

If the same procedure were applicable to asynchronous code, an asynchronous call could be written as follows:

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

The possibility would greatly improve the readability of asynchronous code because the only difference between an asynchronous and a synchronous call would be the use of the keyword yield.

However, the function fs.readFile would then have to be written in such a way that it does not expect a callback, but instead returns an object synchronously, which can be maintained and reacted elsewhere. That’s exactly what Promises allows:

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

The example still does not work because there is still a flow control that responds to the promise and calls intern next. This is what the module co.


ES7: async and await


However, there is no need to use co in the foreseeable future, as the next version of JavaScript, ES7, has built-in support for using the async and await keywords. The keyword async then replaces the generator functions, await replaces yield.

If ES7 were already available today, the load function could be written as follows:

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'); }; 

With the exception of the two new keywords, this corresponds exactly to the synchronous code. In this way, not only the readability improved significantly, but developers can also escape the Callback Hell.

In addition, it is no longer possible to accidentally swallow errors, as async and await ensure that in the case of a rejected promise, an exception is thrown, which must be intercepted with try and catch. Last but not least, the other constructs can also be used for sequential control, for example for loops.

The only catch is that await can only be used in functions that are marked as async. This means that there must be an async function at the top level. However, this can be done easily by using an asynchronous lambda expression as the “main” function that runs automatically:

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

Although the code reads like synchronous code, it behaves asynchronously: Node.js does not block while waiting for the file to finish loading. Under the hood, he still works with promises and callbacks, but for developers, the syntax hides in an elegant way.

This means, however, that an unhandled exception has to be intercepted, since it also appears asynchronously. Therefore, it is advisable to use a top-level global try:

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

Alternatively, you can react to the event process.unhandledRejection:


process.on('unhandledRejection', (reason, p) => { console.error(Unhandled Rejection at: Promise ${p}, reason: ${reason}); }); (async () => { let data = await load('/etc/passwd'); console.log(data); })(); 

Of particular interest is the ability of await to simultaneously wait for multiple asynchronous functions to be executed in parallel. The alternative keyword await *, which is described in the associated proposal, is used for this purpose.

Although the new keywords are not yet finalized in ES7 and ES7 is not yet widely available, the new syntax can still be used. The Babel project makes that possible by offering a compiler that will translate future executable ES2015 and ES7 code into ES5 executable code today.

The easiest way to install Babel globally via npm:

npm install -g babel 

The compiler uses the local version of Node.js as the execution environment. Since the language features of ES7 are still classified as experimental, the support for them should be explicitly activated when calling Babel:


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

Alternatively, Babel can also be installed in other ways. The documentation describes the different approaches.


Conclusion


The uncertainty of the past, which approach should be used for asynchronous programming, is slowly coming to an end: JavaScript supports Promises and will contain the two new keywords async and await in the upcoming version ES7, which simplify the handling of Promises with an elegant syntax.

There is therefore no reason to base new APIs on Promises in order to be prepared for the future. Because Promises are consistently available as part of ES2015, with the exception of Internet Explorer, many do not even need a polyfill.

Since the keywords async and await are syntactically strongly based on their role models in C #, it can be assumed that not much changes in their syntax. Therefore, there is no reason to use them in conjunction with Babel, especially since the project is already establishing itself more and more as the de facto standard in the field of JavaScript compilers.

The biggest challenge of all of this is gradually porting the huge ecosystem of JavaScript and Node.js. The use of functions such as promisify is to be viewed permanently only as a workaround and should be avoided in the long term. Until then, however, the function does a good job of building a bridge between the old and the new world.

Golo Roden

is the founder of “the native web UG“, a company specialized in native weaving techniques. For the development of modern web applications he prefers JavaScript and Node.js and has with “Node.js & Co.” the first German-language book written on the topic.

Source: https://www.heise.de/developer/artikel/Einfuehrung-in-die-asynchrone-JavaScript-Programmierung-2752531.html?seite=all