Learn Asynchronous JavaScript in 2018

Published Jan 10, 2018
Learn Asynchronous JavaScript in 2018

The following is an excerpt from the newest book that I have been working on "Asynchronous JavaScript". You can read it online for free at asyncjsbook.com

If you are new to async programming in JavaScript, you might be surprised with the result of the following piece of code:

setTimeout(function() {
  console.log('1');
}, 0);
console.log('2');

What do you think the output of the above code will be? You might be tempted to say 1 and then 2, but the correct answer is 2 and then 1. In th following sections we will dive into the asynchronous model used in JavaScript and explain why the above code prints 2 and then 1.

Synchronous vs Asynchronous

When you do your daily tasks it's highly likely that you do them asynchronously. Let's look at an analogy to illustrate the difference between the synchronous and asynchronous models of execution.

Imagine that you have three tasks to do in your daily chores list:

  1. Do laundry
  2. Do groceries
  3. Cook dinner

With a synchronous model, you have to finish each task before moving on to the next. That is, you have to finish laundry first before moving onto doing the groceries. If your laundry machine is broken, you cannot do the groceries. The same applies for the third task, you can only cook dinner if and only if you have completed the groceries (and laundry).

Now with an asynchronous model, you don't have to wait for each task to finish to move onto the next. You can start the washing machine and do the groceries while your clothes are being washed. By the time you come back from the grocery store, your clothes are washed. Now if you need to dry your clothes, you can put them in the dryer and cook dinner while your clothes are being dryed.

That's basically the main difference between the synchronous and asynchronous models of execution. In the synchronous model, you have to wait for each task to finish before moving onto the next. But in the asynchronous model you don't have to. You can schedule tasks in a way to effectively do more in less time and not wait if you don't have to. In the next section we will look at the event loop and learn how JavaScript deals with asynchronous tasks.

The Event Loop

Let's look at the snippet that we saw in the beginning of the chapter:

setTimeout(function() {
  console.log('1');
}, 0);
console.log('2');

When you call the setTimeout method, it will be pushed on what is called the message queue. After that, the console.log(2) will be called immediately. After console.log(2) is called the stack empty and JavaScript moves onto the queue and executes what's on the queue. The mechanism that manages this flow is called the event loop. The event loop is responsible for looking at what's on the stack and the queue and scheduling the execution in the right order. In the figure below there are three tasks on the stack to be executed. Once they are finished, two more tasks are picked up from the queue and placed on he stack to be executed:

event-loop.png

Now that's an oversimplified version of the event loop. Obviously it's way more complicated than that, but in essence, the event loop is responsible for listening for tasks that can be executed in the near future.

There are a couple of higher level abstractions like promises and async/await that can help you write clean async code. Let's talk about callback functions, shall we?

Callback Functions

Before talking about callback functions in an async context, it's important to learn how functions can be passed to other functions. Let's look at an example and see how functions can be passed around just like any value in JavaScript.

var name = 'Tom';
hello(name);

In the code snippet above we define a variable called name and we assign a string to it. Then we pass it to the hello function as an argument. We can do the exact same thing with a function. We can define name to be a function instead and pass it to hello:

function name() {
  return 'Tom';
}
hello(name);

Technically speaking name is a callback function because it's passed to another function, but let's see what a callback function is in the context of an asynchronous operation.

In an async context, a callback function is just a normal JavaScript function that is called by JavaScript when an asynchronous operation is finished. By convention, a callback function usually takes two arguments. The first captures errors, and the second captures the results. A callback function can be named or anonymous, but it's better to name them. Let's look at a simple example showing how to read the content of a file asynchronously using Node's fs.readFile method:

function handleReading(error, result) {
  console.log(result);
}
fs.readFile('./my-file.txt', handleReading);

The fs module has a method called readFile. It takes two required arguments, the first is the path to the file, and the second a callback function. In the snippet above, the callback function is handleReading that takes two arguments. The first captures potential errors and the second captures the content.

Below is another example from the https module for making a GET request to a remote API server:

code/callbacks/http-example.js

const https = require('https');
const url = 'https://jsonplaceholder.typicode.com/posts/1';

https.get(url, function(response) {
  response.setEncoding('utf-8');
  let body = '';
  response.on('data', (d) => {
    body += d;
  });
  response.on('end', (x) => {
    console.log(body);
  });
});

When you call the get method, a request is scheduled by JavaScript. When the result is available, JavaScript will call our function and will provide us with the result.

"Returning" an Async Result

When you perform an async operation, you cannot simply use the return statement to get the result. Let's say you have a function that wraps an async call. If you create a variable, and set it in the async callback, you won't be able to get the result from the outer function by simply returning the value:

function getData(options) {
  var finalResult;
  asyncTask(options, function(err, result) {
    finalResult = result;
  })
  return finalResult;
}
getData(); // -> returns undefined

In the snippet above, when you call getData, it is immediately executed and the returned value is undefined. That's because at the time of calling the function, finalResult is not set to anything. It's only after a later point in time that the value gets set. The correct way of wrapping an async call, is to pass the outer function a callback:

function getData(options, callback) {
  asyncTask(options, callback);
}
getData({}, function(err, result) {
  if(err) return console.log(err);
  console.log(result);
});

In the snippet above, we define getData to accept a callback function as the second argument. We have also named it callback to make it clear that getData expects a callback function as its second argument.

Async Tasks In-order

If you have a couple of async tasks that depend on each other, you will have to call each task within the other task's callback. For example, if you need to copy the content of a file, you would need to read the content of the file first before writing it to another file. Because of that you would need to call the writeFile method within the readFile callback:

const fs = require('fs');
fs.readFile('file.txt', 'utf-8', function readContent(err, content) {
  if(err) {
    return console.log(err);
  }
  fs.writeFile('copy.txt', content, function(err) {
    if(err) {
      return console.log(err);
    }
    return console.log('done');
  });
});

Now, it could get messy if you have a lot of async operations that depend on each other. In that case, it's better to name each callback function and define them separately to avoid confusion:

const fs = require('fs');
fs.readFile('file.txt', 'utf-8', readCb);

function readCb(err, content) {
  if (err) {
    return console.log(err);
  }
  return fs.writeFile('copy.txt', content, writeCb);
}

function writeCb(err) {
  if(err) {
    return console.log(err);
  }
  return console.log('Done');
}

In the snippet above we have defined two callback functions separately, readCb and writeCb. The benefits might not be that obvious from the example above, but for operations that have multiple dependencies, the named callback functions can save you a lot of hair-pulling down the line.

I have been working on a book that explains other async abstractions in detail. You can read the book online for free at asyncjsbook.com

Discover and read more posts from Amin Meyghani (AJ)
get started