Asynchronous Javascript

Introduction

JavaScript is a single-threaded programming language that is very popular in Web Development. JavaScript can be synchronous as well as asynchronous. The browser also performs numerous functions for example, various web APIs are accessed by the browser such as setTimeout(), fetch() etc which helps to gain asynchronicity in JavaScript. The role of callback functions in JavaScript is also very important for achieving asynchronicity. It is also important to know the working of JavaScript and understand its behaviour and process of code execution.

Execution of JavaScript code "Synchronously"

JavaScript is a synchronous single-threaded programming language, but we can also perform asynchronous tasks in JavaScript. Before understanding asynchronous Javascript, it is important to understand the synchronous behaviour of JavaScript.

console.log(10); 
const goodFunction = () => {
    console.log('Good'); 
}
const bestFunction = () => {
    goodFunction();
    console.log('Best'); 
}
bestFunction(); 
// 10
// Good
// Best

To understand the above-mentioned example, firstly we need to understand two important terms 'Execution Context' and 'Call Stack'

The Execution Context

As the name suggests, here 'Execution' refers to the JavaScript code execution. Actually, 'Execution Context' is a theoretical or conceptual term in Javascript which refers to the environment where the JavaScript code is executed. This simply means that the context in which the JavaScript code runs or executes in the 'execution context'.

The Call Stack

As JavaScript is a single-threaded programming language, the JavaScript engine has a single call stack that is capable of performing only one action at a time. This single call stack is used by the JavaScript engine for managing execution contexts such as Global Execution Context and Function Execution Context.

We can also say that it is the call stack that keeps track of the functions or the code to be executed and then executes them one by one. This call stack works on the principle of LIFO - LAST IN FIRST OUT. This principle itself says that the code instruction or statement that comes last in the call stack is executed first and then popped off the call stack.

During the code execution, a Global Execution Context or say the execution environment is created, which is represented by main() in the above code and it is pushed to the top of the call stack and as soon as it gets executed, it gets removed from the stack because the call stack executes the code very fastly and it waits for none hence the code inside the call stack is immediately removed after the execution.

The bestFunc() gets called, hence it is pushed to the top of the call stack, then goodFunc() is called, which is inside the bestFunc() hence it is pushed to the top of the call stack then console.log("good") is executed and removed.

So this process of adding, executing and removing code goes on the following LIFO principle until the main() gets removed from the call stack. After all, the notable thing is that the code execution happens inside the call stack sequentially. This is how the code is executed synchronously in JavaScript. This can also lead to a problem called "Blocking".

Blocking

The code execution in JavaScript happens sequentially in the call stack, which may lead to a phenomenon called "Blocking". If there is a code or a function that takes some time to perform its tasks, the code or the functions below or after that code have to wait till the specific function gets executed; hence when the code after a particular code block is blocked due to some delay in that particular code which is "Blocking".

The Callbacks in JavaScript

Functions are first-class citizens in JavaScript, which makes them very powerful with various special features. One such special feature is that the functions can be passed inside other functions as arguments and then they can be used later in those functions. Here, the passed function is called "The callback function".

function One() {
    console.log("One is called");
}
One();
function Two(callback) {
    console.log("Two is called");
    callback();
}
Two(One);
Output:
One is called
Two is called
One is called

In the above-given image, there are two functions: One() and Two(), that are used for explaining the callback functions in JavaScript. As you can see in the image, function One() is passed as an argument inside function Two() and then, after some time, it can be called in function Two() whenever required.

In the above-mentioned example, when we called function One(), the statement inside the function One() got executed and "One is called" got printed in the console. Then we passed the function One() as the callback function inside the function Two(), when we called the function Two() the first statement inside the function got executed and "Two is called" got printed in the console.

Then the second argument got executed, and in that statement, the callback function was called; hence the console.log statement inside the callback function (One()) got executed, and "One is called" got printed in the console. Now we will look at another example of callback functions in JavaScript concerning the setTimeout().

Note that the setTimeOut() method calls the callback function after the specified timer expires. The timer or delays accepts time in milliseconds.

1 second = 1000 milliseconds

2 seconds = 2000 milliseconds

In the above-given example, we have two callback functions. One is an anonymous arrow function inside the first setTimeout() and the other is the normal function inside the second setTimeout().

We have set the timer to 2 seconds in both the setTimeout(). When the program executes "I am from setTimeout() gets printed inside the console two times after the timer of two seconds expires, one from the first setTimeout() and another from the second Timeout().

This is called a callback function because it is called back after some time in the parent function when needed, and then it is executed. It is important to know that the callback functions are very powerful as they provide access to the whole asynchronous world in a synchronous single-threaded programming language that has only one call stack in which the code is executed in a specific order.

<button id="btn">Click here to add h4 heading.</button>
<script>
document.getElementbyId("btn").addEventListener('click', () => {
alert('You are trying to add heading in the document. Click OK to continue');
let heading = document.createElement('h4');
heading.textContent = 'I am h4, added here using an event listener.';
document.body.appendChild(heading);
});
</script>

In the above example, we have used a DOM event listener in JavaScript with the addEventListener() method, the first parameter of addEventListener() is the type of event that is to be listened to, while the second parameter of addEventListener() is an async callback function which is invoked immediately after the event is fired. Here, we have added an event listener to the button tag in HTML. We have specified the 'click' event as the first argument and an anonymous arrow function as the callback as the second argument of the addEventListener() method.

When the user clicks on the button with the id, an alert is thrown on the user's screen with the specified message and when the user clicks ok, the code after an alert is executed, that is the h4 element is added inside the document with the specified content. The callback function which was passed as the second argument in the addEventListener() method is executed asynchronously in the background.

It should be noted that when we pass a callback function to another function, only the function's reference is passed as an argument and due to this we do not add round brackets with the function name while passing it as a callback which means that the callback function inside another function is not executed immediately. It is specifically called back in the parent's function body asynchronously. It is the responsibility of the parent function to execute the callback at its time.

How does Asynchronous JavaScript Work?

Asynchronous as the name suggests means not synchronous. The call stack inside the JavaScript engine can execute only one code at a time and if we have some function like the one which fetches the data from the servers then it can take some time, due to which, the other code below or after that function has to wait for that code to get completed and executed. This is a problem of time hence can be easily solved using a timer or delay. This can be done with the involvement and power of a Browser.

The Browser

We already know that the code is executed in a call stack, this call stack is located inside the JavaScript engine which is located inside the browser.

The browser has access to various things like local storage access, timer access, geo-location access, Bluetooth access, URL etc. with the help of Web APIs. This means that the browser can communicate with the external world. So, we can say that it is the browser that combines the power of Web APIs with the JavaScript call stack and fulfils the need for a call stack such as providing the delay(timer) for some callbacks with the help of the setTimeout() method. So the browser, with the help of Web APIs makes it possible for the code inside the call stack to have a timer available.

The Web APIs

There are various Web APIs like setTimeout(), fetch(), console etc that are used by the browser to fulfil the need of the call stack. Now it is important to know that the setTimeout(), console, and all these Web APIs are not part of JavaScript. These are part of Web APIs and are connected or combined with the call stack of JavaScript Engine with the help of a browser. We can also say that Browser acts as the communicating medium between the call stack and the Web APIs.

const fetchData = (anyURL) => {
return requestedData;
}
function someFunction() {
// function code here
}
fetchData('url here');
someFunction();

In the above example, due to the synchronous behaviour of JavaScript, someFunction() will only get executed when the fetchData() finishes execution. But fetchData() is performing some network requests which can take a considerable amount of time. As fetchData() is going to take some time to fetch the data from some server the code or function after it has to wait for it to complete the execution. It means code after that is blocked and this phenomenon is referred to as "Blocking".

const fetchData = (anyURL) => {
    setTimeout(() => {
        console.log('Asynchronous code here');
    }, 3000);
    return dataFetched;
}
function someFunction() {..}
fetchData('url here');
someFunction();

It should be noted that Web APIs, the event loop, the task queue and the microtask queue are not part of the JavaScript engine, instead, they are part of the browser's JavaScript runtime environment. While in Nodejs, the WEB APIs are replaced by C/C++ APIs

The Event Loop and the Callback Queue

console.log('I am start');
setTimeout(function iAmCallBack() {
    console.log('I am callback inside setTimeout() method');
}, 4000)
console.log('I am end');
Output:
I am start
I am end
I am callback inside setTimeout() method

So during the execution of the above code, a GEC(Global Execution Context) is created, and the first code is pushed to the top of the call stack and executed immediately hence "I am start" gets printed inside the console and then it is popped off from the call stack and next code i.e. setTimeout() is pushed inside the call stack and timer of 4 seconds starts, now the callback which is located inside the setTimeout() is registered in the Web APIs environment and then the next code is pushed to the top of the call stack here "I am end" is console logged and then it is also popped off from the call stack. Meanwhile, the timer that is started by setTimeout() expires and the callback that was registered inside the Web APIs environment is ready to be executed but wait it can not be pushed inside the call stack directly and here comes the Event Loop and the Callback Queue.

The callback which is ready to be executed is sent to the callback queue provided by the browser which is also known as the task queue or message queue. Then the event loop checks whether the call stack is empty or not, if it is empty then the callback which is standing in a queue is sent to the call stack. If it is not empty, then the callback has to wait in the queue till the stack gets empty. Hence, after 4 seconds, the callback function is sent to the task queue and then after the event loop's assurance, it is pushed to the top of the call stack and executed immediately.

But why is there a callback Queue?

It is because of the number of callbacks inside a program. As there can be so many callbacks inside a program, it is important to ensure that all the callbacks get called according to the sequence of occurrence inside the callback queue.

There is one more queue called the microtask queue in which the promises are stored. Note that the callbacks inside the microtask queue are given priority for the execution of the callback inside the task queue.

The Promises in JavaScript

Until now, we have seen a technique to handle asynchronous JavaScript, which was called "Callbacks". Now we are going to see another technique to handle asynchronous JavaScript called Promises. Promises in JavaScript can either be resolved or rejected.

A Promise is an object which has three states: Pending state, Resolved state and Rejected state. As the words suggest, when the promise is not made yet, the state is called a Pending state. When the promise is successfully fulfilled, the state is called Resolved state and when the promise fails to get fulfilled, the state is called Rejected state.

In the above diagram, you can see that the first state of the promise is in the Pending state, then if the promise is resolved, .then(anyFunction) is called for asynchronous operations and if the promise is rejected, the .catch(anyFunction) is called for exception(error) handling. The promise again goes into the pending state if there are multiple promises and the process of .then() and .catch() continues till the last promise. We can use the Promise() constructor to create a new Promise object.

new Promise()
const anyPromise = new Promise(any);

Example of Promises

const promise = new Promise((resolve, reject) => {
    let anyCondition = false;
    if (anyCondition) {
        resolve('Promise is resolved');
    } else {
        reject('Promise is rejected');
    }
}).then((message) => {
    console.log(message);
}).catch((message) => {
    console.log(message);
});
// Output: Promise is rejected

In the above-mentioned code, we have a promise called 'promise' which accepts a function. We have passed an anonymous arrow function with two parameters: resolve and reject. Let us assume that some data needs to be fetched from the server.

Now for simplicity, we assume that if a certain condition is met, then resolve the promise and if it does not meet, then reject the promise. As anyCondition is set to false, it goes to the block where the promise has to be rejected. This further goes to the catch block and prints the rejected message.

If you notice the code, we have attached the .then() method to the promise and the .catch() method to the .then() method. This direct attaching of methods is called "Chaining". This chains the callbacks. Hence, in Promises, the callbacks are chained which eventually makes the code more readable and easier for debugging.

Promises Versus Callbacks in JavaScript

PromisesCallbacks
In Promises, the callbacks are chained, which makes the code more readablePassing callbacks directly leads to a state called 'Callback Hell' which makes the code difficult to read.
Promise callbacks are called in the strict order of occurrence in the microtask queue.Callbacks are not always called in the strict order of occurrence in the task queue
Promise callbacks are given priority while execution as they are stored in the microtask queueCallbacks are not given priority while execution as they are stored in the task queue
Error handling is good with PromisesError Handling is not that good with callbacks
Promises do not lose control of how the function will be executed when passing a callback to a third-party libraryCallbacks lose control of how the function will be executed when passing a callback to a third-party library.

How Promises work

In technical terms, a Promise is an object that is returned as a response to an asynchronous function. It can be one of these four possible states:

  1. PENDING

  2. FULFILLED

  3. REJECTED

  4. SETTLED

The moment a Promise is invoked, it goes to the PENDING state and waits for a response to come. We might get the correct response, which makes the promise FULFILLED or we might get an error i.e., the promise goes to the REJECTED state. A promise is said to be SETTLED if it is not in the pending state .i.e, either the response has arrived or an error has occurred. Once settled, a promise cannot be resettled.

Benefits of Promises

  1. Promises are better than Callbacks as callbacks create a situation called Callback Hell.

  2. Code is readable and better maintained when promises are used to handle async code than when callbacks are used.

There can be two types of tasks that can be carried out in JavaScript:

  1. Synchronous tasks

  2. Asynchronous tasks

JavaScript works very well with synchronous or non-blocking code.

// This function returns data
const getData = () => {
    return [
        {id: 1, name: John},
        {id: 2, name: William}
    ]
}
// Get data and console it
const data = getData();
console.log(data);

We want to getData and then console it. Since getData is an asynchronous task i.e., it will return the data immediately, this piece of code works perfectly fine.

But what if getData needs to request the database to get data and return the data? This task will take time, and hence, this becomes an asynchronous task. Our output, in this case, will be undefined because JavaScript will look for an immediate answer but will not get one. Hence, it will return undefined as data.

When it comes to asynchronous tasks, i.e., where we might not get a response immediately, we need to handle this in a very different way. One of the ways is to consider using callbacks

const getData = (id, callbackFunction) => {
    const data = request data for given id from db
    callbackFunction(data);
}
const data = getData(123, (data) => console.log(data));

We are sending the action to be performed after the completion of the asynchronous task as a parameter to the first function. Suppose we want to filter the data and display an image corresponding to this id.

Note: Loading an image from the database will also need time and is an asynchronous operation.

// This function return the data
const getData = (id, callbackFunction) => {
    const data = request data for given id user from db
    callbackFunction(data);
}
// This function returns the image
const getImage = (data, callbackFunction) => {
    const imageSrc = data.imageSrc;
    callbackFunction(imageSrc);
}
const displayImage = () => {
    // displays the image
}
// This function calls the above functions to get data and then get the image
const displayImageWithHD = (id) => {
    getData(id, (data) => {
        getImage(data, (imageSrc) => {
            displayImage(imageSrc);
        })
    })
}
displayImageWithHD(123);

Now, this can lead to a callback hell

So, we have promises to resolve this issue. then() and catch() in JavaScript help us process the fulfilled and rejected response much more efficiently and easily.

Same code written using promises

//This function returns data corresponding to an id
const getData = (id) => {
    const data = request data for given id user from db
    return data;
}
// This function send the image source from the data
const getImage = (data) => {
    return data.imageSrc;
}
// This function will display the image
const displayImage = () => {
    // displays the image
}
// Main function calling the above functions
const displayImageWithId = (id) => {
    getData(id)
    .then(data => getImage(data))
    .then(imageSrc => displayImage(imageSrc))
};

The flow of the code here is much easier to understand and readable. Hence, some of the evident benefits of using promises are:

  1. Help us handle asynchronous tasks in a much more efficient way

  2. Helps improve the code quality and readability.

  3. Better error handling using catch()

  4. Avoids callback hell and improves the flow of control definition.

Important Rules of Promise

Listed below are the JavaScript ECMAScript 2015 promises rules to follow:

  1. A standard promise comes with a pre-defined .then() method.

  2. A promise is initially in the pending state, which transitions into a fulfilled or rejected state after some time.

  3. Fulfilled or rejected states are settled states. A promise, once settled, cannot be resettled, i.e., a settled state can not transition into any other state.

  4. Once settled, a promise will have value. If fulfilled, the promise will have the data requested. If rejected, it will contain undefined. But it will always have a value after being settled which cannot be changed.

Promise Chaining

.then() always returns another promise. So, we can chain promises like this

fetch(url_to_get_data)
.then(filterData)
.then(consoleData)
.catch(handleError)

Promise chaining works like try and catch but it makes asynchronous code look more synchronous one. The code here works sequentially line by line.

Incumbent settings object tracking

Constructor

  1. Create your own promise using the Promise() constructor.

  2. Pass the executor function as the parameter to the constructor.

  3. The executor function gets executed whenever the promise is created.

We have a promise() constructor in JavaScript. This is used to wrap those functions which do not support promises by default.

Syntax_

new Promise(executor_function)

Executor Function is passed as a parameter to the Promise constructor. It is executed by the constructor function whenever a new promise is created using the constructor.

We can create promises using the Promise constructor in the following code

// Creating a new promise
const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Promise Resolved');
    }, 1000);
});
myPromise.then((value) => {
    console.log(value);
    // expected outpu: "Promise Resolved"
});
console.log(myPromise);
// expected output: [object Promise]

Static Methods

  1. Promise.all(iterable)

    -> This method takes an iterable of promises and resolves to a single promise that resolves when either all the promises in the iterable resolve or anyone rejects them.

    -> Case -1: Resolves when all the promises resolve: In this case, the final resolved value will be an array containing aggregated values from all the promises from the iterable.

    -> Case -2: Resolves when any one of the rejects: In this case, the reason for the rejected promise is the reason the first promise got rejected from the iterable.

// Creating 3 properties
const firstPromise = 60;
const secondPromise = Promise.resolve(25);
const thirdPromise = new Promise((resolve, rejec) => {
    setTimeout(resolve, 100, 'Resolved!');
});
Promise.all([firstPromise, secondPromise, thirdPromise])
.then((values) => {
    console.log(values);
});
// Output: Array[60, 25, 'Resolved!]
  1. Promise.allSettled(iterable)

    -> This method takes an iterable of promises and returns a new promise only when all the promises get settled i.e., are in fulfilled or in the rejected state. This method is mainly used when the promises passed in the iterable are not connected, and we just want to o know what they result in.

    -> On the other hand, Promise.all() is used when the promises are interdependent, and we want to stop the execution when any one of them gets rejected.

const firstPromise = 60;
const secondPromise = Promise.resolve(25);
const thirdPromise = new Promise((resolve, reject) => {
    setTimeout(reject, 100, 'Rejected');
});
Promise.allSettled([firstPromise, secondPromise, thirdPromise])
.then((results) => results.forEach((result) => console.log(result.status)));
// Expected Output
// "fulfilled"
// "fulfilled"
// "rejected"
  1. Promise.any(iterable)

    -> This method takes an iterable of promises, and as soon as one of the promises gets resolved, it gets resolved with the value of the first resolved promise

    -> If none of the promises gets resolved,i.e., all of them get rejected, then the promise gets rejected with an aggregated error formed by grouping all the individual errors.

// Creating 3 Promises
const firstPromise = 60;
const secondPromise = Promisee.resolve(25);
const thirdPromise = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'Resolved!');
});
Promise.any([firstPromise, secondPromise, thirdPromise])
.then((value) => {
    console.log(value);
});
// expected output : 60
  1. Promise.race(iterable)

    -> This method takes an iterable of promises and returns a promise that fulfils or rejects as soon as any one of the promises from the iterable gets fulfilled or rejected.

    -> If fulfilled, the final returned promise has the value of the fulfilled promise. If rejected, it picks the reason from the rejected promise.

// Creating 3 Promises
const firstPromise = new Promise((resolve, reject) => {
    setTimeout(resolve, 300, 'one');
});
const secondPromise = new Promise((resolve, reject) => {
    setTimeout(resolve, 200, 'two');
});
Promise.race([firstPromise, secondPromise])
.then((value) => { console.log(value);
// Boh resolve, but secondPromise is faster
});
// Expected output: "two"
  1. Promise.reject(reason)

    -> This method is used to return a new rejected promise with the reason the same as the one passed as the parameter.

const resolved = (result) => {
    console.log('Resolved');
}
const rejected = (result) => {
    console.error(reason);
}
Promise.reject(new Error('fail')).then(resolved, rejected);
// Expected output: Error: fail
  1. Promise.resolve(reason)

    -> This method returns a resolved promise with the value passed as the parameter. If the value is thenable(i.e., then can be used), it will follow the then's and return the final value.

    -> Note: In cases where we do not know if a value is a promise, promise.resolve() it, and it will return the value as a promise.

Instance Methods

  1. Promise.prototype.then()

    -> It handles the value for the promise in case it is fulfilled successfully. This method takes in two parameters - callback functions for success and failure (both optional) and returns a promise

    -> Syntax: p.then(onFulfilled[, onRejected]); Parameters

    -> onFulfilled: This function is called if the promise fulfils

    -> onRejected: This function is called if the promise rejects.

    Note: Both these functions are optional

let promise = new Promise((resolve, reject) => {
    resolve('Success');
});
promise.then(value => {
    console.log(value); // Success
}, reason => {
    console.error(reason); // Error
});
  1. Promise.prototype.catch()

-> Handles the promise in case an error occurs or the promise gets rejected. If an error occurs, the catch method executes the error handler function passed as a parameter to the catch method.

-> Syntax: p.catch(onRejected); Parameter: onRejected is the function to be called when an error occurs on the promise gets rejected.

const promise = new Promise((resolve, reject) => {
    throw 'Got Rejected!';
});
promise.catch((error) => {
    console.error(error);
});
// Expected Output: Got Rejected!

Note: In the previous section, we handled the error using the second argument to the then() method, and in this section, we are handling the same using the catch() method. Both of them are internally the same. The catch() method internally uses then(onFulfilled, onRejected) to handle the rejected promise! So, a promise can be set to handle errors/rejections both ways.

  1. Promise.Prototype.finally()

-> This method returns a new promise when the promise finally goes to the settled state i.e., it gets fulfilled or gets rejected. This is a handy method when we want to run a piece of code when the promise settles, no matter if it gets rejected or fulfilled.

-> Syntax: p.finally(onFinally); Parameter: onFinally is the function that is executed when the promise either fulfils or gets rejected.

// This function returns a new promise that resolves/rejects
function checkResult(marks) {
    return new Promise((resolve, reject) => {
        if (marks > 32) {
            resolve("Congrats! You've passed!");
        } else {
            reject(new Error('Failed'!));
        }
    });
}
checkResult(56)
.then((result) => {
    console.log(result);
})
.catch((error) => {
    console.log(error);
})
.finally(() => {
    console.log('Result Received');
});
// Output
// Congrats! You've passed !
// Result Received!

Rejected Promises in JavaScript

A promise is an object that contains the results of an asynchronous function. So how do we extract the result from that object once it gets fulfilled successfully? Or how do we get to know that the promise got rejected or if it faces an error? And at last, how do we handle errors/rejections here?

Extracting the Promise fulfilled value

The then() method as discussed earlier in the Instance Methods section is used to extract the result from the async task.

Syntax:

promise.then(value => {
// use value...
});

Extracting the Promise Rejection Error

.catch() method is used to extract the reason for rejection (if the promise rejects).

Syntax:

promise.catch(reason => {
// use value...
});

Extracting Value and Error

Since both .then() and .catch() return a new promise, they can be chained. And both the cases- promise getting fulfilled or getting rejected can be handled using this syntax

promise.then(value => {
    // use value...
}, error => {
    // Check Error
});

How to cancel a Promise

We cannot cancel a promise using modern JavaScript. But you can certainly reject a promise with the reason 'Cancelled'

Note: Do not use .cancel() because it violates the standard rules for creating promises because the function creating the promise should only be allowed to reject or fulfil or cancel that.

Examples

Example 1

const countValue = 2;
let promiseCount = new Promise(function (resolve, reject) {
    if (countValue > 0) {
        resolve("The count value is positive");
    } else {
         reject("Negative Count value!");
    }
});
promiseCount.then(response => {
console.log(promiseCount);
}).catch( reason => console.log(reason));

Explanation: A promise is created using the Promise() constructor in the code above. This promise will resolve or reject based on the value of the count variable.

-> On consoling out promiseCount, we get a promise as the output. But how do we extract the response received? And what about the case our promise rejects because of the negative value of the count variable?

-> We can use the .then() to extract the value of the fulfilled promise and .catch() to extract the reason for rejection in case if rejected.

Example 2

In every web application, we need to send requests to the server for data to be displayed on the browser. This request takes time to come back with data.

fetch("https://www.google.com/javascript")
.then(function(fact) {
    console.log("Fetched Fact: ", fact);
}).catch(function(error) {
    console.error(error);
});

Why was async-await introduced?

Suppose you ordered pizza from dominos and you have to buy shows from a nearby showroom. So instead of wasting time and waiting till pizza is prepared in the meantime you go and buy the shoes.

This is how async/await works.

To solve the callback problem, Promises were introduced in ES2015.

async was introduced to overcome the limitations of promises. Promises used asynchronous code to solve a problem but had their complexity as well as syntax complexity.

Asynchronous code looks like asynchronous(the code gets executed in a sequence that means the next statement has to wait until the previous statement is executed) code but works synchronously.

async

The async keyword stands for asynchronous. It was introduced to solve the issues that were faced by promises. So async works on promises. The work of async is to make the function work without the need of freezing the complete program.

The async keyword is used before the function will return a value. Or we can say that it works as resolved/fulfilled in promise.

Syntax:
async function a() {
    return "Promise";
}
/* async Keyword used before a function */
async function a() {
    return "Resolve";
}
/* If we console.log the function a(), we get the result as - 
Promise {<fulfilled>: "Resolve"}
*/
// We use then function to return Promise
a().then((data) => {
    console.log(data);
});

The "await" keyword

The await keyword is used inside the async function. The work of await is to wait for the execution thread to wait until the result is resolved and then return the result. As await is used inside an async function, it only makes it wait, not the complete program.

Syntax:
/* It is only used inside async function */
let result = await promise;
async function a() {
    let data = new Promise((resolve, reject) => {
        setTimeout(() => resolve("Resolved"), 1000)
    });
    let result = await data; // wait until data resolves
    alert(result);
}
a();

As result, we get an alert displaying Resolved. That means our code is executed within 1 second. The execution of the function gets paused at the line for 1 second because we have set it to await.

Rewriting Promise code with async/await

function returnPromises() {
    let promise = new Promise((resolve) => {
        setTimeout(() => {
            console.log("Promise Executed...");
            resolve("Sample Data");
        }, 1000);
    });
}
function data() {
    let array = ['hi', 1, 2, 3, 5];
    let getPromise = returnPromises();
    console.log(array);
}
data();

With async-await

function returnPromises() {
    let promise = new Promise((resolve) => {
        setTimeout(() => {
            console.log("Promise Executed...");
            resolve("Sample Data");
        }, 1000);
    });
}
async function data() {
    let array = ['hi', 1, 2, 3, 5];
    let getPromise = await returnPromises();
    console.log(array);
}
data();

How does async/await work?

The async keyword was introduced to make the code more efficient than promise and callback functions. The main work of async is to return a promise. We use the async keyword before the function and make that function return a promise. The await keyword can only be used inside an async function, if it is used outside the code will throw an error.

Awaiting a Promise.all() call

Suppose multiple promises need to be fulfilled, if all the promises are executed then it will take time in execution until each of them is resolved, so to overcome this issue Promise.all() is introduced that takes an array of promises as an argument and waits for each promise to be fulfilled and then returns a single promise.

All the promises must not depend on each other.

  • If all the promises are fulfilled then we call the then() handler that contains all arrays of promises in the same order as passed in Promise.all().

  • If any Promise gets rejected then the catch () handler is called with the error that was thrown by the Promise.

const promise1 = 12;
const promise2 = Promise.resolve(1);
const promise3 = 32;
const promise4 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'Hello');
});
Promise.all([promise1, promise2, promise3, promise4])
.then((result) => { console.log(result); })
.catch((result) => { console.log("Some error has occurred."); })
});

How to handle an async-await slowdown

The slowdown means a delay in the execution of a program that can occur due to multiple promises being called in the function. It may be due to multiple arrays or the length property of an array that causes a slowdown. To overcome this issue we use the Promise.all() method.

Handling Errors

We have only seen the promises getting resolved without any errors. But what if there is an error? Sometimes the promise might take some time to reject. Therefore we get a slight delay before the error is displayed by await.

To overcome this issue we use the try...catch.. method

try {
    //code may cause error
}catch(error) {
    // code to handle error
}
async function a() {
    try {
        let promise = await fetch('http://no-url');
    } catch (error) {
        console.log(error);
    }
}
a();

If we don't have a try-catch method and then we can append catch while calling the function.

async function a() {
    let promise = await fetch("http://no-url");
}
Catch().catch((error) => {
    console.log(error);
});

async-await Class methods

To create an object we have to create a class. You can do so my creating a class and declaring an async function inside it.

class A {
    async function() {
        return await Promise.resolve(1);
    }
}
new A().function().then((result) => console.log(result)); // 1

await accepts "thenables"

Thenable in JavaScript is used to enable the async/await function that has a then() function. We can say that all promises are thenable but vice versa is not true. It might happen that the third-party object is not a promise but it is compatible with a promise and supports a .then handler that we can use thenable with await. We can use the thenable object(that has a callback then() handler).

class Thenable {
    constructor(value) {
        this.value = value;
    }
    then(resolve, reject) {
        setTimeout(() => resolve(this.value * 2), 1000);
    }
}
async function data() {
    let result = await new Thenable(10);
    console.log(result);
};
data();

Benefits of using the async function

There are few benefits or async over promise and the callback function.

  • Code Readability: It makes code easy to write and read because async functions can be chained easily using Promise.all() makes it easier to read than with plain promises.

  • We can handle errors easily. Errors can be easily handled using try-catch handlers.

  • async function makes debugging simpler because to the compiler the code looks synchronous.

Thanks for reading the article!!!

happy-coding.gif