How to refactor a chain of asynchronous callbacks in Javascript

From Luis Gallego Hurtado - Not Another IT guy
Jump to: navigation, search


A chain of asynchronous calls in Javascript

It's a a fact that callbacks in Javascript are widely used for asynchronous code. Thus, it's quite common the following scenario:

 1const mongoose = require('mongoose')
 2const post = require('./database/models/post')
 3
 4post.create({
 5    title: 'My first blog post',
 6    description: 'Blog post description',
 7    content: 'Lorem ipsum content.'
 8  }, (error, post) => {
 9    if (error) {
10      console.log(error)
11    } else {
12      //DO SOMETHING
13    }
14  })

The complexity comes when we need to chain on success another asynchronous function which will also received another callback, and so on. In this case, we can end up making our code more and more complex and unreadable.

See below example of creating, then searching and finally deleting elements from a Mongo DB, with mongoose.:

 1post.create({
 2    title: 'My first blog post',
 3    description: 'Blog post description',
 4    content: 'Lorem ipsum content.'
 5  }, (error, post) => {
 6    if (error) {
 7      console.log(error)
 8    } else {
 9      console.log('Created post: ' + JSON.stringify(post))
10      post.find({title: 'My first blog post'}, (error, post) => {
11        if (error) {
12          console.log(error)
13        } else {
14          console.log('Retrieved posts: ' + JSON.stringify(post))
15          post.deleteMany({title: 'My first blog post'}, (error, post) => {
16            if (error) {
17              console.log(error)
18            } else {
19              console.log('Deleted posts: ' + JSON.stringify(post))
20            }
21          })
22        }
23      })
24    }
25  })

The point is that there should be an easy way to get rid of nested indentation, being flexible enough to run N commands. In addition, new design should sort out some caveats, like lack of extensibility for adding a more complex error handling, or a more complex success handling.

The Command Pattern in Javascript

The Command behavioral design pattern helps us to encapsulate the data required to run a command, and decouple it from the execution of the commands themselves.

Functions with same specification

Obviously, create, find and deleteMany are functions with similar arguments and design.

As mentioned before, every execution of any of those functions can be considered to be the instance of a Command. While in Java, we would likely create an interface like MongoDBCommand, in n Javascript, there is not need to do that, as we can just send functions as parameters.

Javascript allows us to send functions as parameters, which help us to send not only the action to execute, but also the success and error handlers.

 1const runCommands = commands => {
 2  const {action, data, onError, onSuccess} = commands.shift();
 3
 4  action(data, (error, post) => {
 5    if (error) {
 6      if (onError) {
 7        onError(error)
 8      }
 9      console.log(error)
10    } else {
11      onSuccess(post);
12      if (actions.length > 0) {
13        runCommands(commands);
14      }
15    }
16  })
17}
18
19const title = 'My first blog post';
20runCommands([
21  {
22    action: post.create.bind(post),
23    data: {
24      title,
25      description: 'Blog post description',
26      content: 'Lorem ipsum content.'
27    },
28    onSuccess: post => {
29      console.log('Created post: ' + JSON.stringify(post))
30    }
31  },
32  {
33    action: post.find.bind(post),
34    data: {title},
35    onSuccess: post => {
36      console.log('Retrieved posts: ' + JSON.stringify(post))
37    }
38  },
39  {
40    action: post.deleteMany.bind(post),
41    data: {title},
42    onSuccess: post => {
43      console.log('Deleted posts: ' + JSON.stringify(post))
44    }
45  },
46])

In previous code, Command Executor function, named runCommand, chains execution of commands in a recursive way. It defines the callback function, so it can easily decorate both, the error and success handling. It can also provide any extra common functionality, like event logging.

Obviously, all functions must have a common design, i.e., in this example:

  • All functions receive a first argument with data to be processed, i.e. element to be inserted, search or deleted.
  • All functions receive a second argument, with the callback that processes 2 arguments: error and data.

Functions with different specification but same callback definition

We may face a case when functions are heterogeneous (different arguments), but they have same callback specification.

In such scenario, we could, however, apply the same design, considering the following changes:

  • Command's data property, would receive an array of parameters, that is, all parameters, but callback.
  • A new property callback index would be part of every Command. I would contain the argument index of callback.
  • Executor would create the callback in the same way we have done in previous example, and then it would insert it into data array in the specified index.
  • Resulting array would be applied to function, by using Javascript apply function.

However, in this case, you may not be really happy with the resulting code. The lack of readability of the resulting code (where de callback index is not obvious), are a trade off you may not willing to have.