DEV Community

Kemi Owoyele
Kemi Owoyele

Posted on

ASYNCHRONOUS JAVASCRIPT

JavaScript is a single threaded programming language. This means that JavaScript run tasks one at a time in a sequence from top to bottom. It goes through our code line by line and executes them as it goes. This process signifies that JavaScript is synchronous by default.

Image description

Synchronous code is much easier to read and write, as the execution flow is clear and easy to follow. On the flip side though, synchronous code can be blocking. This means that in case of a code that might take time to execute, like fetching data from an API; synchronous code will cause the program to wait for the data to arrive before moving on to the next lines of codes. This may lead to performance bottlenecks and waste of time.
Asynchronous code provides a way for us to write code such that codes that takes a while to execute does not block the rest of the execution thread to cause delay of execution. This is usually achieved by passing a callback function as a parameter to execute on the side while the time taking condition is being met. What Asynchronous code does is;
• execute the codes line by line,
• get to the asynchronous code,
• execute the code,
• set aside the callback function pending the required time for the function to run,
• execute next line(s) of codes,
• Execute callback when it is ready.

Asynchronous code are mostly used when fetching data from some database or API endpoints. Ussually, it takes a couple of seconds for these data to arrive from the server, hence we use asynchrounous code to ensure that the execution process is not blocked, pending the arrival of such data.

Image description
Another example of asynchronous code is the javascript setTimeout function. The setTimeout accepts a callback function that will be executed as soon as the stipulated time is completed.
Example;

console.log("line 1")
        console.log("line 2")
        setTimeout(() => {
            console.log("line 3")
        }, 1000)
        console.log("line 4")
        console.log("line 5")
        console.log("line 6")

Enter fullscreen mode Exit fullscreen mode

Image description

HTTP requests

HTTP stands for hypertext transfer protocol. It is a set of rules and guidelines that guide how browsers and servers exchange information. Typically, HTTP requests involve making request to resources from a backend server and the resources get sent back as a response. HTTP requests are made to API endpoints. An Application Programming Interface (API) allows us to have access to data in another system. An endpoint is the specific URL we can use to interact with an API.
There are several types of HTTP requests, they include;

  1. GET: for fetching data from a server
  2. POST: for uploading or submitting data to a server.
  3. DELETE: for deleting data.
  4. PUT: for updating data.
  5. PATCH: used to update parts of a data without replacing the data entirely.
  6. OPTION: checking the HTTP methods available to a resource.
  7. HEAD: getting information about data without retrieving the data
  8. CONNECT: used for establishing a connection to a proxy server.
  9. TRACE: used to test request path, for debugging purposes. In this piece, we are going to focus on the GET request method. To illustrate http requests, we will be using the jsonplaceholder api https://jsonplaceholder.typicode.com. Jsonplaceholder is a free api service that allows us to play around with http requests and receive json data in return.

How to make HTTP request

There are a couple of approaches to handling http requests that have been made available over time. Some of them are;
• xmlHttpRequest object
• promises
• fetch api
• async await
• axios
• ajax etc.
We shall discus some of them.
xmlHttpRequest object
To make a request object, create a variable and assign the xmlHttpRequest object to it;

        const xhr = new XMLHttpRequest();
Enter fullscreen mode Exit fullscreen mode

The request object contains essential information about the request, such as the method, URL, headers, body, and other metadata. This object is what we will use to make a request from the browser. The xhr object comes with a couple of methods and properties.
First, we use the open method to specify the type of request we want to make and the endpoint we want to make it to.

        xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/");
Enter fullscreen mode Exit fullscreen mode

Next, we use the send method to send the request.

        xhr.send();
Enter fullscreen mode Exit fullscreen mode

The response will be delivered to us in json format. json format text are strings that looks like an array of JavaScript objects, but are not objects.
Next we listen for the readystatechange event. The readystatechange event is fired whenever there is a change to the ready state of our request. The ready state represents the current stage of the request. The stages range from 0 to 4. With
0 meaning unsent;
1 meaning opened;
2 headers received;
3 loading
4 fully loaded and request completed.
In the readystatechange event listener we could do what operation we want to do.

xhr.addEventListener('readystatechange', () => {
            console.log(xhr, xhr.readyState)
        })

Enter fullscreen mode Exit fullscreen mode

Image description
As it turns out, we can only do something with the response when the ready state is equal to 4. So we will do a check to see if the readystate is equal to 4

xhr.addEventListener('readystatechange', () => {
            if (readystate === 4) {
                console.log(xhr.responseText)
            }
        })

Enter fullscreen mode Exit fullscreen mode

To make them accessible for presentation, we need to convert the json data into JavaScript objects and assign the outcome to a variable like so;

            const todos = JSON.parse(xhr.responseText)
            console.log(todos)

Enter fullscreen mode Exit fullscreen mode

Image description
At this point, if we had sent the request to the wrong url, or there was a kind of error somehow there would still be no data returned to us.

Image description

Status codes
We would also need to also check for status codes, and verify that the status code is 200. Status codes are numbers that indicate the outcome of an HTTP request. Common status codes include;
• 200 – successful request
• 301-moved permanently to a new URL
• 403 – access denied
• 404 – page not found
• 408-request timed out
• 500-internal server error
• 503- service not available
Etc.

xhr.addEventListener('readystatechange', () => {
            if (xhr.readyState === 4 && xhr.status === 200) {
                const todos = JSON.parse(xhr.responseText)

                console.log(todos)
            } else {
                console.log(`Error: ${xhr.status}`);
            }
        })

Enter fullscreen mode Exit fullscreen mode

Image description
*With wrong url *

Image description

To make all of these codes more reusable, we will wrap them all up in a function and call that function when needed with a callback. The full code will be like;

const getTodos = (callback) => {
            const xhr = new XMLHttpRequest();
            xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/");
            xhr.send();
            xhr.addEventListener('readystatechange', () => {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    const todos = JSON.parse(xhr.responseText)
                    callback(todos)

                } else if (xhr.readyState === 4) {
                    console.log(`Error: ${xhr.status}`);
                }
            })
        }
        getTodos(callback(todos))


Enter fullscreen mode Exit fullscreen mode

If we want to have access to the todos, and do some more things with them in the reusable form, we may have to pass in a callback function to the getTodos function. Then in the callback function we can do whatever we intend to do with the data, like displaying a list of the todos in our UI.

  const getTodos = (callback) => {
            const xhr = new XMLHttpRequest();
            xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/");
            xhr.send();
            xhr.addEventListener('readystatechange', () => {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    const todos = JSON.parse(xhr.responseText)
                    callback(undefined, todos)

                } else if (xhr.readyState === 4) {
                    callback(`Error: ${xhr.status}`, undefined);
                }
            })
        }
        getTodos((err, todos) => {
            const container = document.getElementById('container');

            if (err) {
                container.innerHTML = err;
            } else {
                for (i = 0; i < todos.length; ++i) {
                    let ul = document.createElement('ul');

                    let content = `<li> ${todos[i].id} . ${todos[i].title} </li> 
                 <p> ${todos[i].completed}</p>`;
                    ul.innerHTML = content;
                    container.appendChild(ul);
                }

            }

        })



Enter fullscreen mode Exit fullscreen mode

Add a div with id of container to the HTML, and CSS as desired.

Image description

Promises

JavaScript promise offers a cleaner and more easily maintainable way of handling asynchronous code. A promise is literarily an obligation to do something in the future. When a promise is made, there are three expected outcomes;

  1. Pending: here we are still waiting for the promise to be fulfilled or not.
  2. Resolve: meaning the outcome was successful and promise is fulfilled.
  3. Reject: which means unfulfilled promise with unsuccessful outcome. Syntax
 const getData = new Promise((resolve, reject) => {
            // asynchronous operation
            if (success) {
                resolve(result);
            } else {
                reject(error);
            }
        });

Enter fullscreen mode Exit fullscreen mode

In most cases, we want something to happen when a promise returns successful, say we fetch some data; we want to do something with the data. If the data cannot be fetched, we also want to do something with that outcome as well. The promise approach helps us to handle these two instances with ease. We can call the getTodos function, and tack a .then method to it to handle success cases, and tack a .catch method to handle the error cases.
syntax

const getData = new Promise((resolve, reject) => {
            // asynchronous operation
            if (success) {
                resolve(result);
            } else {
                reject(error);
            }
        });
        getData().then(result => {
            console.log(result)
        }).catch(err => {
            console.log(err)
        })

Enter fullscreen mode Exit fullscreen mode

promise with xmlHttpRequest object

Back to our todos example from the xhr object, we would still create the xhr object and use the relevant methods. The major difference now is that rather than using callbacks to handle what will happen with the data received, or the error as the case may be, we will use the .then() and .catch to handle resolve and reject instances.
Syntax

  const getTodos = (url) => {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.open("GET", url);
                xhr.send();
                xhr.addEventListener('readystatechange', () => {
                    if (xhr.readyState === 4 && xhr.status === 200) {
                        const todos = JSON.parse(xhr.responseText)
                        resolve(todos)
                    } else if (xhr.readyState === 4) {
                        reject(`Error: ${xhr.status}`);
                    }
                })
            })

        }
        getTodos("https://jsonplaceholder.typicode.com/todos/").then(todos => {
            console.log(todos)
        }).catch(err => {
            console.log(err)
        })

Enter fullscreen mode Exit fullscreen mode

promise with fetch API

The fetch API is a built in function that gives us a newer and cleaner way of accessing data asynchronously. In this case, we no longer need to define the xhr object or call the accompanying methods, or to check for ready state change. All of that is taken care of behind the hood by the fetch API.
You can tack a .then function on the fetch method to handle what to do in case of a resolve response, and a .catch method to handle errors.
Syntax

 fetch("https://jsonplaceholder.typicode.com/todos/").then((response) => {
            console.log(response)
        }).catch((error) => {
            console.log(error)
        })

Enter fullscreen mode Exit fullscreen mode

Image description
This will log out to us the response object, and not the exact data we intend to work with. To access the json data, we have to use the json() method on the response object and assign it to a variable. That variable will now allow us to do whatever we want with the data.

  const todos = response.json();
            return todos;


Enter fullscreen mode Exit fullscreen mode

This will return to us another promise. To access the returned data, we will have to chain another .then() method and take the data as an argument.

fetch('todos.json').then((response) => {
            console.log(response)
            const todos = response.json();
            return todos;
        }).then(todos => {
            console.log(todos)
        }).catch((error) => {
            console.log(error)
        })


Enter fullscreen mode Exit fullscreen mode

Another thing to note with the fetch API is that the fetch API checks for ready state change, but does not check for status code. Hence, if the readystate is 4, but status code is not 200, the promise will still resolve but you will not be able to access the data you want to access. So you have to somehow check to ensure that the status of your request is OK, and throw an error if it is not.


            if (response.status !== 200) {
                throw new Error("error! status: " + response.status)
            }

Enter fullscreen mode Exit fullscreen mode

If you try a non-existing address now, you can get a custom error message.

Image description

To make the code reusable, we can wrap the whole thing up in a function.

      const getTodos = (url) => {
            fetch(url).then((response) => {

                if (response.status !== 200) {
                    throw new Error("error! status: " + response.status)
                }
                const todos = response.json();
                return todos;
            }).then(todos => {
                console.log(todos)
            }).catch((error) => {
                console.log(error)
            })
        }

        getTodos("https://jsonplaceholder.typicode.com/todos/")

Enter fullscreen mode Exit fullscreen mode

async and await

Another new introduction to javascript asynchronous programming is the use of async and await keywords. These keywords make it easier to chain promises together, and provide even simpler and easy to read and debug syntax for handling asynchronous operations.
The async keyword is used to declare that the function is an asynchronous function. Whereas the await keyword, is used to tell that the codes in the async function should wait for the response or rejection of the statement before proceeding with the rest of the function. The await keyword can only be used inside an async function.
Syntax

        async function doSomething() {
            const result = await getSomething(); // wait for the result 
            console.log(result); // continue with the rest of the code
        }

        //           with arrow function
        const doSomething = async () => {
            const result = await getSomething(); // wait for the result 
            console.log(result); // continue with the rest of the code
        }


Enter fullscreen mode Exit fullscreen mode

So back to our getTodos example,

        const getTodos = async () => {
            const response = await fetch("https://jsonplaceholder.typicode.com/todos/");
            console.log(response)
        }
        getTodos();

Enter fullscreen mode Exit fullscreen mode

Image description
We would still need to use the json() method on the response;

            const todos = await response.json();
            return todos;

Enter fullscreen mode Exit fullscreen mode

And also to throw an error if status code is not equal to 200;

 if (response.status !== 200) {
                throw new Error("error! status: " + response.status)
            }

Enter fullscreen mode Exit fullscreen mode

So now we will have;

        const getTodos = async (url) => {
            const response = await fetch(url);
            if (response.status !== 200) {
                throw new Error("error! status: " + response.status)
            }
            const todos = await response.json();
            return todos;

        }

Enter fullscreen mode Exit fullscreen mode

To call this function with our desired url and chain the relevant response and error handling functions;


        const getTodos = async (url) => {
            const response = await fetch(url);
            if (response.status !== 200) {
                throw new Error()
            }
            const todos = await response.json();
            return todos;
        }
        getTodos("https://jsonplaceholder.typicode.com/todos/")
            .then(todos => {
                console.log(todos)
            })
            .catch(err => {
                console.log(err)
            })

Enter fullscreen mode Exit fullscreen mode

Image description
With the wrong address

Image description

Summary

JavaScript is synchronous by default. This means that JavaScript executes code sequentially. But with data that requires some time to execute, there has to be some way to handle those instances without causing delay in execution or block the flow of execution.
There are different approaches available to handling these kinds of operations. Here, we discussed the xhr request object, promises, fetch, and async & await. These approaches were built on one another, as the methods improved over time. The newer approaches made the codes easier to write and read. Which will in turn make the codes easier to maintain and debug

Top comments (0)