Embracing expressiveness of Callbacks

As a javascript developer you will have come across the term – “Callback hell”. This term might run the chill down a developer’s spine than a complex business requirement or algorithm. The term which you have to wrestle just to make sure that any another developer can view and manage the code written by you. As much as frightening the term sounds, callbacks are unavoidable elements of any piece of javascript code you will write today. All frameworks and development styles have embraced callbacks despite the challenge they pose.

Code written with a single timeout function will not make you lose your sleep worrying about callback but it is only over time these problems becomes menace. The following code you will not really agree on existence of challenge in the first place –

 

setTimeout(function() {

   console.log(‘Timeout expired’);

}, 3000);

 

 

Trouble with callbacks starts stirring up in situations like these –

 

const https = require(‘https’);

const fs = require(‘fs’);

 

fs.readdir(‘some path’, {withFileTypes: true}, function(err, entries) {

   if(err) {

       console.error(‘Error occurred while getting the list of directory entries – ’ + err);

   } else {

       entries.forEach(function(entry, index) {

           if(entry.isFile()) {

               console.log(‘Current file being processed is – ’ + entry.name);

               fs.readFile(‘some path’ + entry.path, ‘utf-8’, function(error, data) {

                   let codeLookup = https.get(‘some url’+data, function(httpResponse) {

                       if(httpResponse.statusCode != 200) {

                           console.error(‘Code lookup failure – ’ + httpResponse.statusMessage);

                       } else {

                           let lookupData = ‘’;

                           let parsedLookup = {};

                           httpResponse.on(‘data’, function(chunk) {

                               lookupData += chunk;

                           });

                           httpResponse.on(‘close’, function() {

                               parsedLookup = JSON.parse(lookupData);

                           });

                       }

                   });

               });

           }

       });

   }

});

 

The example above reads the file system for all files and performs a lookup. Seems to be related to a data processing activity. This activity is just the beginning, imaging transformation to data or any secondary lookup and more importantly writing the file back in case we wanted to save the data. With these exclusions itself we have data nested at 8 levels. Wondering how we arrived the number 8? It is easy, count the number of curly braces at the bottom of the code sample.

But here is the flip side of this problem. Let us re-write the code slightly different –

 

const https = require(‘https’);

const fs = require(‘fs’);

 

function pringErrorAndExit(err, message) {

   if(err){

       console.error(message + err);

       process.exit();

   }

}

function parseAnyHttpError(httpResponse) {

   if(httpResponse.statusCode != 200) {

       console.error(‘http call error – ’ + httpResponse.statusMessage);

   }

}

function readFileAndLookup(fileData) {

   let codeLookup = https.get(‘some url’, function(httpResponse) {

       if(httpResponse.statusCode != 200) {

           console.error(‘Code lookup failure – ’ + httpResponse.statusMessage);

       } else {

           let lookupData = ‘’;

           let parsedLookup = {};

           httpResponse.on(‘data’, function(chunk) {

               lookupData += chunk;

           });

           httpResponse.on(‘close’, function() {

               parsedLookup = JSON.parse(lookupData);

           });

       }

   });

}

 

fs.readdir(‘some path’, {withFileTypes: true}, (err, entries) => {

   pringErrorAndExit(err, ‘Error occurred while getting the list of directory entries – ’ + err);

   entries.forEach(function(entry, index) {

       if(entry.isFile()) {

           console.log(‘Current file being processed is – ’ + entry.name);

           fs.readFile(‘some path’ + entry.path, ‘utf-8’, function(error, data) {

               parseAnyHttpError(httpResponse);

               readFileAndLookup(data);

           });

       }

   });

});

 

 

Now if you see we have a nesting of 4 visible levels. We already halved the visible levels of nesting. Use the same rule as above to count the levels at the bottom of the code snippet. This by no means have no effect on cyclomatic complexity of the entire codebase, just in case you are used to that measure. This is a rearrangement of code to increase maintainability. Needless to say; higher maintainability score will lead to lesser defects.

We have achieved this by a balanced refactoring. The balance is between a simple and first level suggestion which you will find in many online resources – “Name the functions” and other aspect of refactoring is expressiveness of the code. Let us explain what we mean by that. If we follow the recommendations of “Name the functions” we will end up getting a code like this –

 

 

const https = require(‘https’);

const fs = require(‘fs’);

 

function parseDirectoryEntry(err, data) {

   // Processing code…

   data.forEach(iterateEntries);

}

function iterateEntries(data, index) {

   // Processing code…

   fs.readFile(‘some path’+data.path, readFileAndLookup);

}

function readFileAndLookup(fileData) {

   // Processing code…

}

function pringErrorAndExit(err, message) {

   // Error printing code.

}

function parseAnyHttpError(httpResponse) {

   // Error printing code.

}

function readFileAndLookup(fileData) {

   // Processing code…

}

 

fs.readdir(‘some path’, {withFileTypes: true}, parseDirectoryEntry);

 

Though concise as much as possible, a look at this does not reveal the activities performed. Additionally, to get an entire picture of what this file achieve you will have to walk to each function and inspect for activities they perform.

The code sample shown above that requires a look at “fs.readdir” to get an idea of what is the intent of the developer. Anyway no one can agree more refactoring is always an art involves balancing of many facets.

Crafty refactoring could take us only this far but another tool in toolbox of javascript as a language can help us achieve much more than this crafty work. We are talking about “Promises”.

Developers working with modern libraries might overlook the promises but definitely will have seen the usage of it. So, let us take a moment to peek at the code create promises using the same scenario.

const directoryListing = new Promise( function(resolve, reject) {

   //processing code here…

});

With promises our code to parse each file and use that to lookup will look like this –

 

const https = require(‘https’);

const fs = require(‘fs’);

 

function pringErrorAndExit(err, message) {

   if(err){

       console.error(message + err);

       process.exit();

   }

}

 

function parseDirectoryEntry(err, data) {

   // Processing code…

   if(data.isFile()) {

       return new Promise((resolve, reject) => {

           resolve(data);

       });

   }

}

function iterateEntries(data, index) {

   // Processing code…

   data.forEach((entry, index) => {

       return new Promise((resolve, reject) => {

           resolve(entry);

       });

   });

}

function getFileContent(fileName) {

   // Processing code…

   return fs.readFile(fileName, ‘utf-8’);

}

function performLookup(fileData) {

   // Processing code…

   https.get(‘some url’ + fileData, processHttpResponse);    

}

function processHttpResponse(httpResp) {

   if(httpResponse.statusCode != 200) {

       console.error(‘Code lookup failure – ’ + httpResponse.statusMessage);

   else {

       let lookupData = ‘’;

       let parsedLookup = {};

       httpResponse.on(‘data’, function(chunk) {

           lookupData += chunk;

       });

       httpResponse.on(‘close’, function() {

           parsedLookup = JSON.parse(lookupData);

       });

   }

}

function pringErrorAndExit(err, message) {

   // Error printing code.

}

 

fs.readdir(‘some path’, {withFileTypes: true})

   .then(parseDirectoryEntry)

   .then(iterateEachEntry)

   .then(getFileContent)

   .then(performLookup)

   .catch(pringErrorAndExit)

 

 

 

Now if any developer looks at this code will the intent not appear? We guess it will be very straight forward. In fact, the art of retaining expressiveness from the refactoring is taken away by the “then” blocks with named functions. The journey can be taken one more level up with modules i.e., with export keyword. We leave that to you for exploration. We cannot conclude without making this point – Callbacks are what you know as function pointer in object-oriented languages. Promises are built atop these concepts, as you

 

would notice it is 3 level nesting of function pointer for a promise. Async and Await are syntactic sugar for “.then” and “.wait” which are used to chain i.e. define order / synchronize promises. Hope these points help you link the sleek tricks discussed here with larger knowledgebase of other articles in the web which talk about callbacks and promises in that language.

In fact, we started out straight at the heart of the problem. We never talked about why you should spend your few seconds reading through this article. We believe if you have come this far, your have experienced the problem of managing nested callbacks arising out of using any modern javascript framework to develop your cool application. The problem becomes manyfold if you had to mix both callback style with promises. Given the fundamentals here in this dispatch above, we hope you will easily manage the scenarios where you would have to mix both of them.

 

picture courtesy- medium.com