Another stab at event-loop, callbacks, promises and async-await

Another stab at event-loop, callbacks, promises and async-await

Handling blocking code

A plain Javascript function is simply a bunch of lines that are bound to the function scope, and are sequentially executed when the function is called.

// Plain JS function
function plainOldFn() {
  console.log('A');
  console.log('B');
  console.log('C');
}

plainOldFn(); // Outputs A, B & C on separate lines

Now, knowing that JavaScript is a single-threaded language, how can we run multiple tasks in parallel? This is done through the event loop and the callback queue. Let's say we have a function that fetches data from an URL via an XHR call (or maybe some file from the file system in Node.js). Since the execution engine has to wait for the HTTP response (or on the OS kernel), this can take an unknown amount of time depending on the network (or the file-system r/w speeds). While we can synchronously do this, the execution will be blocking, and so any line of code that is independent of the result of this call will also have to wait. So what we do instead is we send this function (as sort of a message) to the callback queue. The event loop keeps monitoring the callback queue and the function call stack, and whenever the call stack is empty it pops a message from the queue and starts running it.

What's the benefit of calling asynchronously: Say we want to do multiple parallel HTTP calls (or IO/OS-kernel dependent calls). And we want to do some further processing on the result of these calls. These function calls will now not be executed right away. They are passed on to the callback queue and are executed only when the call stack is empty.

For example, let's make an HTTP call to the Star Wars API and fetch the first planet. And in parallel let's perform some independent tasks.

// Asynchronous way for calling function
import { get } from 'https';

function blockingFn() {
  get({
    hostname: 'swapi.dev',
    path: '/api/planets/1/',
  }, function(response)  {
    let data = '';
    response.on('data', function (d) { data += d; });
    response.on('end', function() {
      const planet = JSON.parse(data);
      console.log(`Received planet: ${planet.name}`);
    });
  });
}

function cheapIndependentFn() {
  console.log('The cheap independent function is now done');
}

blockingFn(); // This gets passed on to the callback queue
cheapIndependentFn(); // Gets called while we are waiting on the HTTP response

This pattern is so much preferred in the Node.js world that Node.js doesn't even provide a default synchronous way to make HTTP calls. This is also the reason why even when Node.js provides both ways, like when performing an IO operation of reading a file viafs.readFile(asynchronous) and fs.readFileSync(synchronous), the asynchronous way is always preferred in almost all use cases.

What are promises?

The https.get function above takes 2 arguments. The first is an object containing the hostname and path. The second is a function that is executed only when the HTTP response has arrived. These kinds of functions whose time of execution is dependent upon the completion of some asynchronous operation are called callback functions.

Now let's say you want to fetch a resident of this planet. Of course, we can't do this until the planet is fetched. So we have to time the function execution to be called only when the first planet has been received.

Here's how that would look like:

function fetchPlanetResident() {
  get({
    hostname: 'swapi.dev',
    path: '/api/planets/1/',
  }, function(response)  {
    let data = '';
    response.on('data', function (d) { data += d; });
    let planet = null;
    response.on('end', function() {
      planet = JSON.parse(data);
      get({
        hostname: 'swapi.dev',
        path: planet.residents[0],
      }, function(response)  {
        let data = '';
        response.on('data', function (d) { data += d; });
        let resident = null;
        response.on('end', function() {
          resident = JSON.parse(data);
          console.log(`Received resident: ${resident.name}`);
        }); 
      });
    });
  });
}
fetchPlanetResident();

As you can see, our get call for the resident is nested within the callback function for fetching planet. Say now we want to fetch a vehicle that the resident owns. This will again, only be possible after fetching the resident. Thus the HTTP call will be inside the callback function for fetching residents. This becomes much harder to understand and debug in a production environment. That is why the promise was introduced into JS in 2015 (ES6).

A promise is a special JavaScript object which has two internal states: pending and fulfilled. The promise starts with a pending state when the async call has not been completed, i.e, it is in the callback queue, or is currently being executed in the call stack. Once the execution is complete, it transitions into the fulfilled status. In the fulfilled status, it can be either in a rejected state, in case something unexpected happened and an error was thrown, or in a resolved state if all went okay while executing.

While these statuses are internal, the public interface that promises expose consists of a then and a catch function. The then function can be provided two callback functions - one for when the asynchronous function call did not error out, and the other when it did. These asynchronous functions are supplied the value and the error objects correspondingly by the JavaScript runtime.

Other than that the promises also provide a finally interface which can be provided a function to perform cleanup actions. Note that finally isn't passed on the value or error object. And finally can also be followed by subsequent then and catch functions, in which case finally just passes on to the value or error onto them from the previous then or catch.

Here's how fetching a planet would look using promises. We are using the node-fetch library because the default Node.js https library doesn't use promises. So go ahead and do: npm install node-fetch.

import fetch from 'node-fetch';

function fetchUsingPromises() {
  const promise = fetch('https://swapi.dev/api/planets/1/');
  promise
    .then(response => response.json())
    .then(planet => console.log(planet.name));
}
fetchUsingPromises();

And here's how chaining HTTP calls that are dependent on previous calls, would look like.

function chainingPromises() {
  fetch('https://swapi.dev/api/planets/1/')
    .then(response => response.json())
    .then(planet => planet.residents[0])
    .then(residentUrl => fetch(residentUrl))
    .then(response => response.json())
    .then(resident => console.log(`Resident name: ${resident.name}`));
}
chainingPromises();

As you can see, the chaining is no more nested, rather sequential like in other programming languages. This helps a lot in writing maintainable code that is easy to understand and debug.

Async-await

Later in 2017, async-await was introduced into JavaScript, which is another layer of abstraction over promises (another fancy way of returning promises). With async-await, we don't even have to chain the dependent functions in then or catch blocks. We turn our functions into async functions, (because async-await can only be applied to functions and is not supported at the top level) which can return resolved/rejected promises.

Thus an async-await implementation of fetching planet would look something like:

async function usingAsyncAwait() {
  const response = await fetch('https://swapi.dev/api/planets/1/');
  const planet = await response.json();
  const residentUrl = planet.residents[0];
  const resident = await (await fetch(residentUrl)).json();
  console.log(resident.name); // Printed second
}

usingAsyncAwait()
  .then(() => { console.log('Planet resident now fetched') }); // Printed last
console.log('An independent function call'); // Gets printed first

As you can see from the sequence of the console.log statements, synchronous actions are performed first and then asynchronous functions are called sequentially. Whether to use async-await or promise-then-catch is completely up to the user. Generally, I prefer to use async-await whenever inside functions (because it makes code even simpler to understand, almost identical to what you would write in other programming languages), and I resort back to promises outside functions.

Hope this helped clear some of the concepts related to the asynchronous execution paradigm in JavaScript. Some really good resources to clarify things even further are:

  1. Event loop, callback queue and related concepts
  2. callbacks
  3. promises
  4. async-await