Introduction
This blog explains the fundamental concepts that JavaScript relies on to handle asynchronous operations
. These concepts include Callback functions
, Promises
, and the use of Async
, and Await
to handle deferred operations in JavaScript.
So before we decode the comparison between the three, let's get a brief understanding of synchronous (blocking) and asynchronous(non-blocking).
Difference Between Synchronous and Asynchronous
To understand this, let's say you want pizza, but your wife wants Chinese food and your kids want burgers.
Synchronous calls
would be like going to the pizza place, order, wait, take the pizza, go to the Chinese place, order, wait, take the food, go to the burger place, order, wait, take the food home. This takes a long time, but you at least know for sure that each order was completed properly before you go on to the next one.
But on the other hand, Asynchronous
would be like calling the pizza place and ordering delivery. Then call the Chinese place and order delivery. Then call the burger place and order delivery. Then wait. All 3 will show up (eventually, you hope), but you don't know what order they will show up in.
Synchronous functions
will always happen in the order that you told them to. That is, one function will not start until the last one has finished.
Asynchronous functions
won't wait for other functions to start or finish; they'll just do what the function does as fast as they feel like doing it.
There are different ways to handle the async code. Those are callbacks, promises, and async/await.
The following shows the syntax of the Promise.allSettled()
method:
Promise.allSettled(iterable); //The iterable parameter is a list of input Promises.
Callback in Javascript
If you’re familiar with programming, you already know what functions do and how to use them. But what is a callback function? Callback functions are an important part of JavaScript.
So in this post, I would like to help you to understand what callback functions are and how to use them in JavaScript by going through some examples.
What is a Callback Function?
In JavaScript, functions are objects. Can we pass objects to functions as parameters? Yes.
So we can also pass a JavaScript Callback Function parameter to another JavaScript function. Let me show that in an example below:
function mahi(callback) {
callback();
}
The above code represents a Javascript function.
To understand how functions work, check out my other blog JavaScript Functions.
function funOne(x) {
return x;
};
function funTwo(var1) {
// some code
}
funTwo(funOne);
This method of passing in functions to other functions is used in JavaScript libraries almost everywhere.
The common name for the function passed in is a callback function
.
In computer programming, a callback
is a piece of executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at some convenient time.
The invocation may be immediate as in a synchronous callback
or it might happen at a later time, as in asynchronous callback
.
We will come back to synchronous and asynchronous callbacks in the subsequent sections.
To understand what I’ve explained above, let me start with a simple example. We want to log a message to the console but it should be there after 3 seconds.
const text = function() {
console.log("lol"); // This message is shown after 3 sec
}
setTimeout(message, 3000);
There is a built-in method in JavaScript called setTimeout
, which calls a function or evaluates an expression after a given period of time (in milliseconds). So here, the “text” function is being called after 3 seconds have passed. (1 second = 1000 milliseconds)
Callback as an Arrow Function
If you prefer, you can also write the same callback function as an ES6 arrow function, which is a newer type of function in JavaScript:
setTimeout(() => {
console.log("This message is shown after 3 seconds");
}, 3000);
The output will be the same as above.
The problem with callbacks is it creates something called Callback Hell
.
Basically, you start nesting functions within functions within functions, and it starts to get really hard to read the code. So in this situation Promises came to handle the nested callback in a better way.
Promises
Prior to the start, let's understand the actual syntax of it.
A promise
in Javascript is just like a promise in real life, what you do is you commit something by saying I promise to do something, For example : I promise to make the best blog on promises
, and then the promise either has two results:
Resolved
: indicates that the promised operation was successful.Rejected
: indicates that the promised operation was unsuccessful.
So if I give you the best blog ever on promises
then I would Resolve
my promise to do so, but if I failed to give you the best blog ever on promises
then that would be Rejected
because I was not able to complete the promise
.
Now let's look at the actual syntax of creating a promise.
Creating a simple promise
A promise is created using a constructor that takes a call back function with two arguments.
const p = new Promise((resolve, reject) =>{
let a=1+1;
if(a==2) {
resolve("Promise is resolved!")
} else {
reject('Promise is rejected!')
}
});
p.then(( successMsg ) => {
console.log("Success:" + successMsg)
}).catch((errorMsg) => {
console.log("Error:" + errorMsg)
}) //output : Success :Promise is resolved!
In the above code, if the promise
is true then we resolved
success, else rejected.
- To use the above created Promise we use then() for resolve and catch() for reject.
So in our created promise
condition was true
and we called p()
then our console logs read:
Success: Promise is resolved!
So if the promise gets rejected, it will jump to the catch() method and console logs differnt message:
Error: Promise is rejected!
What is Chaining?
Callback functions have been used alone for asynchronous operations in JavaScript for many years. But in some cases, using Promises can be a better option.
If there are multiple async operations to be done and if we try to use good-old Callbacks for them, we’ll find ourselves quickly inside a situation called Callback hell:
firstRequest(function(response) {
secondRequest(response, function(nextResponse) {
thirdRequest(nextResponse, function(finalResponse) {
console.log('Final response: ' + finalResponse);
}, failureCallback);
}, failureCallback);
}, failureCallback);
However, if we handle the same operation with Promises, since we can attach Callbacks rather than passing them, this time the same code above looks much cleaner and easier to read:
firstRequest()
.then(function(response) {
return secondRequest(response);
}).then(function(nextResponse) {
return thirdRequest(nextResponse);
}).then(function(finalResponse) {
console.log('Final response: ' + finalResponse);
}).catch(failureCallback);
The code just above shows how multiple callbacks can be chained one after another. Chaining is one of the best features of Promises.
Promise.all( )
Promise.all is actually a promise that takes an array of promises as an input (an iterable). Then it gets resolved when all the promises get resolved or any one of them gets rejected.
For example, assume that you have ten promises (Async operation to perform a network call or a database connection). You have to know when all the promises get resolved or you have to wait till all the promises resolve. So you are passing all ten promises to Promise.all. Then, Promise.all itself as a promise will get resolved once all the ten promises get resolved or any of the ten promises get rejected with an error.
Promise.all([Promise1, Promise2, Promise3])
.then(result) => {
console.log(result)
})
.catch(error => console.log(`Error in promises ${error}`))
As you can see, we are passing an array to Promise.all. And when all three promises get resolved, Promise.all resolves and the output is consoled.
Promise.allSettled
ES2020 introduced the Promise.allSettled()
method that accepts a list of Promises
and returns a new Promise
that resolves after all the input Promises have settled, either resolved or rejected.
The returned Promise resolves to an array
of objects
that each describes the result of each input Promise. In turn, each object in the array contains two properties: status
and value
(or reason). The status can be fulfilled
or rejected
. And value (or reason) reflects the value which each promise was fulfilled
(or rejected
) with.
Error Handling
One of the biggest benefits of using promises is the way they allow you to handle errors. Fortunately, promises allow you to replace repetitive checks with one handler for a series of functions.
The error handling API for promises is essentially one function named catch. However, there are some extra things to know when using this function. For instance, it allows you to simulate an asynchronous try/catch/finally sequence. And it’s easy to unintentionally swallow errors by forgetting to rethrow them inside a catch callback.
Promises swallow exceptions by default.
This does include exceptions such as ReferenceError
so that the following code runs perfectly fine, without ever letting you know that something went wrong:
var myPromise = Promise.resolve();
myPromise
.then(function() {
console.log("Step 1");
})
.then(function() {
console.log("Step 2", thisNameDoesNotExist);
});
What happens here is that the implementation of Promise wraps our function in a try-catch block. Instead of throwing the error and causing our interpreter to crash (but print a convenient error message at least), it catches it and marks the promise p as rejected.
var myPromise = Promise.resolve();
myPromise
.then(function() {
console.log("Step 1");
})
.then(function() {
console.log("Step 2", thisNameDoesNotExist);
})
.catch(function(e) {
console.log("Promise rejected, error:", e);
});
In theory. In reality, though, it is very easy to forget to specify the .catch() handler at the end, have it in the wrong place, etc.
For that reason, Bluebird
, one of the most popular Promise libraries, has a “default” onRejection handler which will print all errors from rejected Promises to stderr. This is a huge improvement and while it can still result in unexpected behavior because it doesn’t interrupt execution, it at least gives a hint where things are dodgy.
Async/Await
There’s a special syntax to work with promises in a more comfortable fashion, called async/await
. It’s surprisingly easy to understand and use.
Await
is basically syntactic sugar for Promises. It makes your asynchronous code look more like a synchronous/procedural code, which is easier for humans to understand.
The syntax of async
function is:
async function functionName(parameter1, parameter2, ...paramaterN) {
// statements
}
Example:
async function getPromise(){}
const promise = getPromise()
Even though getPromise
is literally empty, it’ll still return a promise
since it was an async
function.
If the async
function returns a value, that value will also get wrapped in a promise
. That means you’ll have to use .then
to access it.
async function add (x, y) {
return x + y;
}
add(2,3).then((result) => {
console.log(result) // 5
})
You can see that we use the “async” keyword for the wrapper function add
. This lets JavaScript know that we are using async/await
syntax, and is necessary if you want to use Await
. This means you can’t use Await
at the global level. It always needs a wrapper function. Or we can say await is only used with an async
function.
Hope you enjoyed the article. Happy coding!
If this article was helpful, Tweet it!