Asyncronous programming in JavaScript

Introduction

Understanding asynchronous programming in JavaScript is fundamental to using JavaScript. Asynchronous means several operations occurring at the same time.

This guide provides and a simple introduction to asynchronous programming in JavaScript. It covers the basics and not everything there is to asynchronous programming in JavaScript.

Fork, clone or download sample project here sample project

Remix the project at glitch.io

JavaScript was initially developed to add interactivity to html elements on a page. For instance, when a page loads, the JavaScript gets loaded then parsed. A button on the page sits there waiting for a click mouse event. We attach a click event listener to the button element and a callback function to the event to be triggered when the click event fires.

const loginBtn = document.getElementById('login-btn')
loginBtn.addEventListener('click', callbackfn)

Asynchronous programming is when two or several operations happen at the same time. In JavaScript, asynchronous programming is highly used and a good grasp is ideal.

Let’s say one has a page that displays the coin market cap(Price and volume) of various crypto. You’d asynchronously fetch the data from an API, while the page continues to render during page load. Once the results become available, we render the results on the webpage.

JavaScript offers three ways of performing asynchronous actions:

  • using callbacks
  • Using Promises
  • Async-await - Most recent development introduced in ES7 version

1. using callbacks

callbacks are functions passed to other functions as values. They are “inline” functions with standard function signature and arguments. They can be arrow functions or ES5 functions.

// A simple signature of a callback
const waitUp = (someArgs, callback) => {
  setTimeout(() => {
    // mimick a delay, e.g fetching data from and api
    const fakeData = {
      user: 'Kakashi sensei',
      age: 27,
      village: 'Hidden Leaf'
      leadership: '4th Hokage'
    }

    // pass the data to the callback function argument, we will provide this when we call waitUp later in our program

    callback(fakeData) // we then can handle the data we got inside this callback
  }, 3000)
}

// consuming the callback and handling the data asyncronously returned by waitUp

waitUp('', (data) => {
  console.log(data) // our data is now available for use
})

Callbacks are common in Nodejs, latest versions of Nodejs provide ES6 promises which are more cleaner to use.

2. using promises

Promises are a new standard introduced in the ES6(ES2015) version. Promises represent proxy values that are yet to resolve.

When consuming a promise, promises exist in three states:

  • pending state
  • resolved state
  • rejected state

While performing operations that do not resolve immediately such as fetching data from a web API or reading file contents from a disk, results from the operation won’t immediately be available for use within your program. Promises make it less painful to perform such operations.

// creating a promise, note the new syntax

const waitUp = () => 
  return new Promise((reject, resolve) => {
    // mimick a delay, e.g fetching data from and api
    setTimeout(() => {
    const fakeData = {
      user: 'Kakashi sensei',
      age: 27,
      village: 'Hidden Leaf',
      leadership: '4th Hokage'
    }
// pass date to resolve, to resolve the promise with a value
    resolve(fakeData) // we finally resolve with a value once we get the data
  }, 3000)
})

// consuming the promise created
  waitUp()
    .then((data) => {
      // do something with the data
    })
    .catch((err)=> {
    // handle the promise rejection
    })

Consuming a promise is a matter of chaining the function call with a .then(() => {}) You may chain as many “dot-thens” to the call as possible. However, using promises quicky becomes convoluted and leads to code that is hard to follow as the number of “dot-thens” become hard to follow.

Fetch API uses promises as we shall see. Fetch API provides a cleaner way of making HTTP request from the browser. No more XMLHttpRequest

fetch('http://heroes.glitcth.io')
  .then((res) => res.json()) // parses the body into JavaScript object literal
  .then((data) => console.lo(data))
  .catch((err) => console.log(err)) // .catch comes last to catch handle any errors when the promise is returns an error

In most cases, consuming a promise would be more common especially when making HTTP request using a library like axios and other HTTP tooling.

3. async-await

Async-await is a syntactical sugar for promises that was introduced in ES2017 version to make using promises more cleaner. To use async-await:

  1. Declare a function async by adding the async keyword to the function signature.
// an async function
async function waitUp(args) {

}

// in arrow functions
const waitUp = async(args) => {

}

a 2. To perform any asyncronous calls inside the function / expression you declared async, prepend await to the call, like:

async function waitUp() {
  const res = await fetch('https://glitch.io/heroes')
  const data = await res.json()
  // use the data like in a normal function
  console.log(data)
}

// to handle promise rejections
async function waitUp() {
  try {
    const res = await fetch('https://glitch.io/heroes')
    const data = await res.json()
    // use the data like in a normal function
    console.log(data)
  } catch(ex) {
    // any exceptions thrown are caught here
  }
}

handling rejected promise is as easy as wrapping your asyncronous code inside a try { } catch(ex) { } block Async-await makes handling promise cleaner and less painful. You are more likely to see alot of async-await in modern JavaScript than promises and callbacks.

Promises and async-await are interoperable, this means what can be done using promises can be done using async-await.

For example: This implementation becomes:

const waitUp = new Promise((reject, resolve) => {
  // do some operations that won't returns a valu
    setTimeout(() => {
    // mimick a delay, e.g fetching data from and api
    const fakeData = {
      user: 'Kakashi sensei',
      age: 27,
      village: 'Hidden Leaf',
      leadership: '4th Hokage'
    }

    // pass the data to the callback function argument, we will provide this when we call waitUp later in our program

    resolve(fakeData) // we finally resolve with a value once we get the data
  }, 3000)
})

// consuming the promise created
  waitUp()
    .then((data) => {
      // do something with the data
    })
    .catch((err)=> {
    // handle the promise rejection
    })

Becomes:

const waitUp = new Promise((reject, resolve) => {
  // do some operations that won't returns a valu
    setTimeout(() => {
    // mimick a delay, e.g fetching data from and api
    const fakeData = {
      user: 'Kakashi sensei',
      age: 27,
      village: 'Hidden Leaf'
      leadership: '4th Hokage'
    }

    // pass the data to the callback function argument, we will provide this when we call waitUp later in our program

    resolve(fakeData) // we finally resolve with a value once we get the data
  }, 3000)
})

// consuming the promise created using async-await
// assuming a main function somewhere:

const main = async() => {
  const data = await WaitUp()
  // use the data like in a syncronous function call
  console.log(data)
}

main() // calling main

Summary

Understanding the asynchronous programming aspect of JavaScript is crucial Constant practise and using promises in a project helps solidify understanding usage of Promises. Async-await does not replace promises but makes code cleaner and easy to follow. No more .then(fn) chains


Follow me on twitter @nkmurgor where I tweet about interesting topics.