How to avoid infinite nesting callbacks

Performing something after an asynchronous call has ended requires you to place subsequent program logic inside a callback. Performing multiple steps leads to deeply nested callbacks.

Is there any way avoid deeply nested callbacks?

There are two good ways to flatten nested callbacks. They both have to do with waiting for a Promise to resolve a value. The first one uses ES6 generators and the yield keyword. The second one builds on top of that with async functions and the await keyword. Both methods allow you to write flat program code free of nested callbacks.

Placing logic inside callback

When using callbacks, you're required to put dependent program logic that is to be executed after an asynchronous operation has completed logic inside a callback. When you combine multiple such calls, you end up with deeply nested code.

Deeply nested callbacks in JavaScript code.
Deeply nested callbacks in JavaScript code.
Source: http://slides.com/michaelholroyd/asyncnodejs

Let's write a program that reads two files and prints their lengths. When using callbacks, our example looks like the following.

1
 var fs = require('fs');
2 
3 fs.readFile('file1.txt', 'utf8', (err, file1) => {
4     fs.readFile('file2.txt', 'utf8', (err, file2) => {
5         console.log(`total length: ${file1.length + file2.length}`);
6     });
7 });

Yield

ES6 generators and the yield keyword can be used write flat asynchronous code. You need a Promises-based version of your API together with a special helper function. The responsibility of the helper function (also known as co/coroutine/spawn function) is to coordinate yield and waiting for Promises to resolve to a value. One implementation of such helper function is Bluebird.coroutine.

The example using generators and yield would look like the following. Notice how flat the program structure is - there are no callbacks. The function* () {} denotes a generator function where you can use the yield keyword.

1 var fs = require('fs-promise');
2 var bluebird = require('bluebird');
3 
4 bluebird.coroutine(function* () {
5     var file1 = yield fs.readFile('file1.txt', 'utf8');
6     var file2 = yield fs.readFile('file2.txt', 'utf8');
7 
8     console.log(`total length: ${file1.length + file2.length}`);
9 })();

Await

The approach of using generators to yield Promises seen previously is so good that there will be built-in language support for it in ES2017. It will be called async functions and they will use the await keyword instead of yield.

The example using async functions and the await keyword looks like the following. Notice how the call to coroutine and passing of a generator function has been replaced with async function.

1 var fs = require('fs-promise');
2 
3 (async function() 
{
4     var file1 = await fs.readFile('file1.txt', 'utf8');
5     var file2 = await fs.readFile('file2.txt', 'utf8');
6 
7     console.log(`total length: ${file1.length + file2.length}`);
8 })();

You can use async functions and the await keyword natively in new JS engines that support it or use TypeScript or Babel to convert code into the preceding specification of JavaScript.

Avoid infinite nesting callbacks

Use ES6 generators and yield or ES2017 async functions and await to write flat asynchronous code instead of infinite nesting callbacks. You can use yield in native today and async functions with the help of TypeScript or Babel.

Related articles

cover

There are 350 000 NPM packages. Your task: pick one.

Learn what to look for in a reliable NPM package.

Get Guide

Share article: