Asynchronous programming in JavaScript - Promises, Callbacks and Async-await

naftalimurgor

Naftali Murgor

Posted on December 10, 2021

Asynchronous programming in JavaScript - Promises, Callbacks and Async-await

Asynchronous programming in JavaScript

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 callback function to the event to be triggered when the click event fires.

const loginBtn = document.getElementById('login-btn')
loginBtn.addEventListener('click', () => {
  // do something when 'click' mouse event fires i.e button      // is clicked
})
Enter fullscreen mode Exit fullscreen mode

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 an 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
})
Enter fullscreen mode Exit fullscreen mode

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((resolve, 
  reject) => {
  // 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 parameter, 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
    })
Enter fullscreen mode Exit fullscreen mode

Consuming a promise is a matter of chaining the function call with a .then(() => {}) You may chain as many "dot-thens" to the function 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.glitch.io')
  .then((res) => res.json()) // parses the body into JavaScript object literal
  .then((data) => console.log(data))
  .catch((err) => console.log(err)) // .catch comes last to catch handle any errors when the promise  returns an error
Enter fullscreen mode Exit fullscreen mode

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

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) => {

}
Enter fullscreen mode Exit fullscreen mode
  1. 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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 return a value immediately
    setTimeout(() => {
    // mimick a delay, e.g fetching data from an 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 we created above
  waitUp()
    .then((data) => {
      // do something with the data
    })
    .catch((err)=> {
    // handle the promise rejection
    })
Enter fullscreen mode Exit fullscreen mode

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 an api
    const fakeData = {
      user: 'Kakashi sensei',
      age: 27,
      village: 'Hidden Leaf'
      leadership: '4th Hokage'
    }

    // pass the data to the resolve callback

    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
Enter fullscreen mode Exit fullscreen mode

Summary

Understanding the asynchronous 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.

Do you feel stuck with learning modern JavaScript? You may preorder Modern JavaScript Primer for Beginners where I explain everything in a clear and straight-forward fashion with code examples and project examples.

This article was orignally published at naftalimurgor.com
Thanks for stopping by!

đź’– đź’Ş đź™… đźš©
naftalimurgor
Naftali Murgor

Posted on December 10, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related