Understanding asynchronous javascript

Understanding asynchronous javascript

Introduction

You may have come in contact with applications that become unresponsive when a long-running task is executing, this is because the application is running a synchronous task that blocks the execution of subsequent tasks, which result in a user experience that is slow and less appealing. This is prevented by asynchronous programming which increases the performance and responsiveness of an application.

JavaScript by default is a synchronous, blocking, and single-threaded language, this means JavaScript executes code in sequential order from the top of the script to the bottom, and until a task is completed, the subsequent tasks cannot be executed hence described as “blocking ”. It is single-threaded because it has one call stack(this is where JavaScript codes are executed), which means it can only execute one command at a time. However, it has asynchronous capabilities(non-sequential order) and this is important because it prevents time-consuming operations such as fetching data from a server from blocking other operations.

In this article, I will lay the foundation for understanding why javascript can perform some tasks asynchronously and go to more lengths in explaining how to implement asynchronous behaviour with the use of callbacks, promises, and the async and await keywords.

Prerequisite

Basic knowledge of JavaScript. Though written for beginners, this is not a basic topic in JavaScript.

What are synchronous and asynchronous operations?

To paint a picture of synchronous and asynchronous functions we can consider two different scenarios below.

The first is waiting in a queue in a bank to perform transactions at the automated teller machine. The persons, before you have to be done with all the transactions, they hope to perform, before you can step in to perform yours, and until you're done with your transactions, no matter how long that might be, the persons behind you cannot step in to perform theirs. This is essentially a synchronous blocking action.

let us expatiate on this using some code examples.

Console.log("hello world")

Let x = 1000000000

While(x > 1) {
    x -= 1
}
Console.log(“this might take a while”)
Console.log(“this is taking so long”)

When you run this on your editor, you'll notice that after the “hello world” is printed to the console, there is a time lag before the other two statements are printed to the console as well and this is because the while loop takes a longer time to be executed as accounted for by the lag. If we add more zeros to x, you will notice the time lag increase, adding additional zeros will make the while loop run to infinity, preventing the last two statements from getting printed to the console. This is Javascript at its core, synchronous and blocking.

For the second scenario, consider you're at a restaurant that accepts both fiat and mobile bank transfer(which in this case might take a bit of time to be received by the restaurant) as a form of payment and you have to make payments before your meal is given to you, it's your turn to get served, the chef/waiter dishes out your meal, you decide to make payments via mobile transfer for everything you will be having while waiting for your payment to reflect in the account of the restaurant, the chef or waiter can start attending to other customers, and when it finally reflects your order can be completed and handed to you. This is an example of an asynchronous action.

To demonstrate asynchronous behavior in JavaScript, we'll use the setTimeout() function, the function accepts two parameters, a timer, and a callback function to run when the time elapses.

console .log(“This is the first input”) 

console.log(“This is the second input”) 

setTimeout(() => { console.log(“This is the third input”) }, 3000)

console.log(“This is the fourth input”)

// Output

// This is the first input

// This is the second input

// This is the fourth input

// This is the third input.

The first and second input is printed to the console but rather than wait for 3000 milliseconds(3 seconds) at one point, Javascript goes ahead to execute the fourth input below the third input while it waits for the time to elapse. While demonstrating asynchronous behavior with setTimeout() the timer can be used to mimic the time it might take for a server to respond to a network request.

Why Javascript can perform asynchronous functions.

Every browser comes with a javascript engine that executes any javascript code contained inside a web application, and it has just one call stack(this is where javascript is executed). When a function is called it is pushed to the top of the call stack and when the function returns it is popped from the top of the call stack. However, web APIs such as setTimeout(), fetch API, XMLHttpRequest, DOM etc. are implemented by the browser itself and not the Javascript engine, the request for the web API originates within the JavaScript environment but the execution of the API call runs outside of it, in the browser. These web APIs as can be found in this MDN documentation are not core JavaScript language and therefore makes javascript asynchronous as they run on a different thread in the browser.

They are usually associated with a callback function that governs what task we carry out with the data when it returns, as in the case of fetch API, when the time elapses as in the case of setTimeout() function or when an event occurs as in the case of event listeners. Once the web API is done with its job, it binds the result of that job to the callback function and pushes it to the callback/task queue, this is a queue where callbacks are stacked on. An event loop occasionally checks to see if the call stack is empty, if it is, it picks up the task/callback in the task queue and puts it in the call stack to be executed.

Patterns of implementing Asynchronous Behaviour in JavaScript

Callbacks

One of the ways of implementing asynchronous action in javascript is the use of callbacks.

A callback is a function passed into another function as an argument which is then invoked inside the outer function. We have seen how callbacks worked with asynchronous functions when we used the setTimeout() function.

The main focus of this article is to use HTTP requests to demonstrate asynchronous behavior, so for this section, we'll use XMLHttpRequest(the old-fashioned way of sending requests) to send requests to the server so that we can understand better how callbacks work with requests.

To make an API call to an API endpoint using XMLHttpRequest, we'll start by instantiating a new XMLHttpRequest object, calling the open and the send method, to send the request.

Const getUser = () => { 
    const request = new XMLHttpRequest(); 
    request.open('GET', resource); 
    request.send(); 
}

XMLHttpRequest works with the use of event listeners which as we have seen above is a web API capable of asynchronous behavior, the event is a “readystatechange”. There are four readyState you can check it up here Mdn documentation, the event handler which is a callback function is fired when there is a state change. In the call back we will attach a conditioner that checks if the ready State === 4 signifying that the operation is done and the response has returned and if the status code is 200, signifying that the operation was successful and we have our data (“responseText”). The data is usually a JSON string, so to convert it to a JSON object that can be accessed by JavaScript we will use the parse method.

Const getUser = (resource) => {
   const request = new XMLHttpRequest();

request.addEventListener('readystatechange', () => {
   if (request.readyState === 4 && request.status === 200){
   Const data = JSON.parse(request.responseText)}
})
   request.open('GET', resource);
   request.send();
}

In this article, we will make use of endpoints from JSONPlaceholder. we will create a callback that handles/ print's the data to the console and also handles the errors we get.

Const getUser = (callback) => {
   const request = new XMLHttpRequest();

request.addEventListener('readystatechange', () => {
   if (request.readyState === 4 && request.status === 200){
   Const data = JSON.parse(request.responseText)
   callback(undefined, data)
}else{
     callback(‘cannot fetch data’, undefined)
}
})
   request.open('GET', "https://jsonplaceholder.typicode.com/users");
   request.send();
}

getUser((err, data) => {
    if(err) {
        //handles error
        console.log(error)
    }else{ console.log(data); // handles data
})

The code above is a modification of the previous one to accept just callbacks and then we input the API endpoint into the resource.

We can see that callback makes our code reusable and easier to work with, looking at the code again, we can pass in a different callback when we want to call the getUser again.

for some instances, we might want to make several network requests sequentially or a series of asynchronous functions dependent on the completion of the previous function as shown in the code below.

 getUsers("https://jsonplaceholder.typicode.com/users/1", (err, data)=> {
    console.log(data)
    getUsers("https://jsonplaceholder.typicode.com/users/2" (err, data)=>{
        console.log(data)
        getUsers("https://jsonplaceholder.typicode.com/users/3",(err,data)=>{
            console.log(data)
        })    
    })
})

One of the drawbacks of using callbacks this way is running into what is called a “callback hell”.

Callback hell occurs when we nest callbacks into callbacks in trying to perform more tasks dependent on the completion of a preceding function, making our code less readable and difficult to manage/debug.

firstFunction(args, function(){
   secondFunction(args, function(){
       thirdFunction(args, function(){
           fourthFunction(args, function(){     
          //more callbacks(Callback hell) 
           })
        })
    })
})

Promises help make up for this drawback by allowing us to attach callbacks or chain them rather than nesting them in a function.

PROMISES

Promises are the modern way of implementing asynchronous javascript. A promise is an object representing the eventual completion or failure of an asynchronous function. It can exist in three states; pending, fulfilled, or rejected depending on the state of the request. The promise object provides two inbuilt methods; resolve and reject, that handle the result of an operation. Successful completion is indicated by the resolve function call and errors are indicated by the reject function call. The good thing about using promises is that we can chain them together and this is an advantage when we want to execute two or more asynchronous operations back to back, where each subsequent operation starts when the previous operation succeeds, likely with the result from the previous step.

In this section, we will refactor our previous codes to return a promise so we can perform several asynchronous operations easily.

const  getUser = (resource) => { 

    return new Promise((resolve, reject) => {
        const request = new XMLHttpRequest(); 
        request.addEventListener('readystatechange', () => {
       if (request.readyState === 4 && request.status === 200){
           Const data = JSON.parse(request.responseText)
           resolve(data);
       }else{
          reject('error getting resource');
}
})
        request.open('GET', resource); 
        request.send(); 
    })

}
getUser('"https://jsonplaceholder.typicode.com/users"')

In the code above, we instantiated a new promise and returned the promise object, so we can chain it to another promise using the then() method.

The then() method of a Promise object takes in two arguments; callback functions for the fulfilled and rejected cases of the Promise, It immediately returns an equivalent Promise object, allowing us to chain calls to other promise objects. With the then() method we can work with promise-returning functions.

The callback function for the rejected cases is optional and usually not defined here because of the existence of another method; the catch() method; The catch() method of a Promise object schedules a function to be called when the promise is rejected. It covers all promises so that there is no need to define callbacks for the rejected promise with each iteration.

getUser("https://jsonplaceholder.typicode.com/users").then(
    (onResolved)=> { 
    //performs a task or return a promise}
    },
    (onRejected) => { // handles error
    }
    ).catch()

With promises we can perform a series of asynchronous functions (chaining promises) as opposed to nesting callbacks as shown below;

getUser("https://jsonplaceholder.typicode.com/users").then(data => {
  console.log("promise 1 resolved;", data)
  return getUser("https://jsonplaceholder.typicode.com/users/1");
)}.then(data => {
  console.log("promise 2 resolved;", data)
  return getUser("https://jsonplaceholder.typicode.com/users/2");
)}.then(data => {
  console.log("promise 3 resolved;", data)
  return getUser("https://jsonplaceholder.typicode.com/users/3");

Looking at the above code we can see using promises is relatively easier to read, manage and debug than nesting callbacks, each then() method returns a promise whose value is handled by a succeeding function.

Using the fetch API()

The fetch API is the modern, promise-based replacement for XMLHttpRequest. It implements the promise API behind the scenes making handling success and error cases easy, we don't need to call a new Promise and create the resolve and reject method as it already does that under the hood, leading us to write lesser codes. In this section, we'll be using the fetch API to make the same HTTP request we did in the previous section with XMLHttpRequest.

//fetch api
fetch("https://jsonplaceholder.typicode.com/users").then((response) => {
  console.log('resolved', response);
  return response.json();
}).then(data => {
   console.log(data)

fetch is a function that takes in the API endpoint as a parameter and returns a promise object, to print the result of our request to the console we attach the then() method. depending on the number of operations we want to perform on our data we can attach a lot of functions with the then() method.

async and await

The async keyword provides a more simplistic approach to working with promise-based code. Adding async before the function makes it an asynchronous function. Inside an async function, we can use the await keyword before a call to a function that returns a promise, it suspends the execution of the promise-returning function until the returned promise is fulfilled or rejected.

Using the same function and API endpoint, we can now create an asynchronous function.

const getUser = async () => {

  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  const data = await response.json();

  return data; 
}
getUser().then(data => console.log('resolved', data))

It is important to note that the async keyword must always be used to create an async function, the await keyword can only be used inside a function with an async keyword and before a function that returns a promise as we can see it used before the fetch function.

Conclusion

If you have read all through to this point, you likely now have an understanding of what Asynchronous JavaScript is, how it is possible for javascript to perform asynchronous operations and how it is implemented with the use of callbacks, promises and the async and await keyword.

This article explained the basic concepts, and to get more familiar with them, it is important to practice more coding. Good luck as you do.