This page looks best with JavaScript enabled

JavaScript: Write your asynchronous code linearly

 ·  ☕ 6 min read  ·  ✍️ Iskander Samatov

Introduction

One of the building blocks of the Node.JS ecosystem are asynchronous processes. Asynchronous processes in Node.JS are non-blocking, which means the main execution threads does not wait for the asynchronous process to finish.

This fact introduces coding style that is fundamental to working with Node.JS – Continuous Passing Style, or CPS for short. With CPS, you start an asynchronous process and pass a callback function to it, the callback gets executed when the process is finished.

Callbacks

The most common problem that developers face when working with CPS is called callback hell. Callback hell is when your code start growing horizontally rather than vertically, slowly shaping into a pyramid:

1
2
3
4
5
6
7
8
fs.readFile('/path/to/file', (err, data) => {
    const body = data.toString()
    request(url, (err, response, body) => {
        fs.writeFile('newFileName',body => {
            
        })
    })
})

As you can see, the method above is taking a shape of a pyramid and it only does three simple things:

  • Read file
  • make http request
  • write to a file

Most of the Node.JS developers are familiar with callback hell so I’m not going to get into too much detail.

Promises

Then, over time, things got better and when the new JavaScript specification called EcmaScript 2015(or ES6) came out, we got a solution to our callback hell problem – promises.

Promises provide a more elegant solution for writing sequential asynchronous processes. You can read more on promises here , but in a nutshell, promises let you avoid the pyramid structure of your code by chaining multiple asynchronous executions with then.You pass a function to then which can return another promise, thus continuing the chain.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
readFile()
  .then(data => {
      return postToServer(data);
  })
  .then(err, body => {
      return writeToLocalFile(body)
  })
  .then(err => {
      if (!err) {
        return console.log('success!')
      }
      console.log(err)
})

You can already see that the code looks much better compared to the callback-based solution. And if we keep adding promises to the chain the nesting level of the promises will stay the same, it won’t grow horizontally.

async await

Promises are great, but in my opinion that’s still a lot of boilerplate code just to accomplish trivial asynchronous sequence. Wouldn’t it be great if we could make asynchronous calls one after the other, similar to how it’s done in other programming languages, like Java?

Good news is there’s a new EcmaScript proposal that addresses that problem. I’m talking about async and await keywords. Let’s look at the example:

1
2
3
4
5
async  function doSomethingAsync() {
    const data = await readFile()
    const response = await postToServer(data)
    console.log('success')
}

This looks a lot better! We were able to reduce our previous operation to just 3 lines of code and it’s more readable. To make it all work we had to do 2 things:

  • Put async keyword before the function keyword when declaring a new function
  • Whenever we have a promise to execute we put await before the promise call to make the main execution thread wait till the promise returns

async and await weren’t a part of the first ES2015 specification, they were added later on. Still, today Node 7 + and most of the modern browsers support them. However, if you’re looking for a full support you might need to use JavaScript transpilers, such as Babel.

Generators

One last solution I want to mention has to do with ES2015 generators. Generators are functions that let you use yield keyword to pause the execution of the generator function. Let’s take a look at the example:

1
2
3
4
5
function* generatorExample() {
    yield 'Hello World'
    yield 2 + 2
    console.log('success')
}

Interesting thing to notice here is that you need to put * after the function to declare a generator. Also, whenever the execution code inside of the generator encounters yield keyword, it pauses the execution and awaits to be resumed again.

Here’s a sample code on how we would use our generator:

1
2
3
4
const genr = generatorExample() // returns new generator instance
genr.next()
const hello = genr.next().value
const four = genr.next().value

Calling a generator function returns a new instance of that generator. We call next method on that instance three times: one to start the function execution, and the other two to resume after yield keyword is encountered. Another thing you will notice is that next function returns an object. The object returned from next has the following format:

1
2
3
4
{
  value: yielded value,
  done: whether the generator is done executing or not
}

Generators by themselves do not make our asynchronous code any easier, we need to use them with third party libraries. One of these libraries is called co .

Co let’s you use generators to write your asynchronous code in a linear fashion. Co works with several types of yieldables:

  • Promises
  • Thunks
  • Generators
  • Generator functions
  • Objects
  • Arrays

Let’s take a look at the example of using co with generator function and promises:

1
2
3
4
5
co(function* () {
    const data = yield readFile()
    const response = yield sendRequest(data)
    yield writeToFile(response)
});

From the code above you can see that using co and generators lets us write code which seems to be executed in a linear fashion. However, truth is that there’s a complex callback system that wraps our generator to make it possible. But you don’t need to worry about that.

async await vs Generators

One advantage of using co over async is that co also deals with other yeildable types such as thunks, generators and array of promises, while async only deals with promises. Plus, co has built-in helper methods to execute an iteration of async tasks in a sequential or parallel fashion.

So to summarize:

  • Using vanilla callback-based approach to asynchronous execution might lead to deep nesting problem called ‘Callback Hell’
  • Promises are a step in the right direction to solving the deep nesting problem, but they still generate a lot of boilerplate code
  • Using async and await, we can reduce our async execution code, while also making it more intuitive, however this approach might not be supported by older browsers
  • Another method of of achieving a linear code in our asynchronous execution is using generators combined with libraries like co

Conclusion

In this brief post we talked about the tricks and techniques we can use to write simpler and shorter asynchronous code.

The beauty of Node.JS platform is that there are multiple ways to achieve this simplicity. And a lot of it also comes from the new syntax introduced by EcmaScript 2015+.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.