How to refactor a chain of asynchronous callbacks 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:

const mongoose = require('mongoose')
const post = require('./database/models/post')

post.create({
    title: 'My first blog post',
    description: 'Blog post description',
    content: 'Lorem ipsum content.'
  }, (error, post) => {
    if (error) {
      console.log(error)
    } else {
      //DO SOMETHING
    }
  })

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.:

post.create({
    title: 'My first blog post',
    description: 'Blog post description',
    content: 'Lorem ipsum content.'
  }, (error, post) => {
    if (error) {
      console.log(error)
    } else {
      console.log('Created post: ' + JSON.stringify(post))
      post.find({title: 'My first blog post'}, (error, post) => {
        if (error) {
          console.log(error)
        } else {
          console.log('Retrieved posts: ' + JSON.stringify(post))
          post.deleteMany({title: 'My first blog post'}, (error, post) => {
            if (error) {
              console.log(error)
            } else {
              console.log('Deleted posts: ' + JSON.stringify(post))
            }
          })
        }
      })
    }
  })

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.

Functions with same specification

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

In other languages, every execution of any of those functions would be named as Command. In fact, in Java, we would likely create an interface like MongoDBCommand. However, in Javascript, there is not need to do that, as we can just send functions as parameters.

const runCommands = commands => {
  const {action, data, onError, onSuccess} = commands.shift();

  action(data, (error, post) => {
    if (error) {
      if (onError) {
        onError(error)
      }
      console.log(error)
    } else {
      onSuccess(post);
      if (actions.length > 0) {
        runCommands(commands);
      }
    }
  })
}

const title = 'My first blog post';
runCommands([
  {
    action: post.create.bind(post),
    data: {
      title,
      description: 'Blog post description',
      content: 'Lorem ipsum content.'
    },
    onSuccess: post => {
      console.log('Created post: ' + JSON.stringify(post))
    }
  },
  {
    action: post.find.bind(post),
    data: {title},
    onSuccess: post => {
      console.log('Retrieved posts: ' + JSON.stringify(post))
    }
  },
  {
    action: post.deleteMany.bind(post),
    data: {title},
    onSuccess: post => {
      console.log('Deleted posts: ' + JSON.stringify(post))
    }
  },
])

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.