Asynchronous JavaScript - Promise, Web APIs, Callback queue
Aug 14, 2022
•13 min read
If you try to search JavaScript over the web, you usually get two types of statements.
JavaScript is a single-threaded, synchronous, blocking programming or scripting language.
- Let's break this statement down;
Single-threaded: JavaScript is single-threaded in nature because the browser executes the complete script in a single execution thread. JavaScript engine executes our program sequentially one after another, statement by statement. - Synchronous: JavaScript is synchronous in nature because everything is executed in sync. The next statement (program) has to sit and wait until the currently executing one is completed. Once the program is completed, the next one becomes active.
- Blocking: JavaScript executes our programs sequentially, statement by statement. JavaScript engine blocks the execution of the next program until our current program is completed. Does not matter how long our program is taking to complete, JavaScript would never bypass our program, thus blocking the rest of the code.
Dual Faced JavaScript
Well at some points in your research, you might have also encountered somewhere something like;
JavaScript is asyncronous in nature.
JavaScript is non-blocking programming language
Now see what this means actually;
- Asynchronous: JavaScript is asynchronous means, it does not wait for a program to complete if it is taking too long, it simply bypasses it and moves to the very next program.
- Non-blocking: Similarly, JavaScript doesn’t block the execution of our program if our current program is taking longer. JavaScript parks our guilty program and moves to the next program, thus non-blocking in nature.
What the hell?
Well, let's clear our confusion. JavaScript is synchronous and asynchronous at the same time. Well, no doubt, JavaScript is synchronous by default. But that doesn’t mean you cannot run it asynchronously. We can make JavaScript run our code asynchronously by using various means. Which we will be discussing in a short while.
Synchronous JavaScript
I am pretty sure you are already well aware of the synchronous face of JavaScript, well we will be talking about Asynchronous JavaScript in this guide. Consider this very simple program which is handled synchronously(default behavior);
This program logs everything in sequence,
Program Start
→ Function Start
→ Function End
→ Program End
You are already well aware of this behavior so we will not be wasting any time on this.
Guide Outcomes
Follow along, at the end of this guide you will be able to answer all of these questions.
- What is Asynchronous JavaScript? And why do we need asynchronous JavaScript?
- How do we implement Asynchronous JavaScript in our code?
- What are Web APIs? And what Web APIs can help write Asynchronous JavaScript code?
- What are callback functions? And we can write asynchronous code with them?
- What are Promises in JavaScript? And what is their role in modern asynchronous JavaScript?
- What are the microTask queue and the Callback queue in JavaScript?
What is Asynchronous JavaScript? And why do we need asynchronous JavaScript?
As we already know JavaScript is synchronous in nature by default. It executes every program sequentially and blocks the next program until the completion of the currently executing program.
But, What if we need do not need to run our code immediately? We need to park our function to be executed in the future under specific conditions.
What if our program takes a bit longer to complete? Our complete program would freeze until its completion.
What if our program needs some external data, and makes an API call to the server? Of course, depending on the speed of the server, it would take some time to receive a response from the server.
I can give you many scenarios, which make asynchronous JavaScript necessary.
How do we implement Asynchronous JavaScript in our code?
We can write asynchronous JavaScript logic in our program in a number of ways. Modern JavaScript and Web browsers provide us with these major means to write asynchronous JavaScript code;
- Web APIs
- Callback functions
- Promises (replacement of callback functions)
Web APIs
To understand Web APIs properly, we need to know a little behind-the-scenes of JavaScript. For those who do not know, our browser is the place where JavaScript code is executed. Our browser provides our code with a special environment called the Runtime Environment. Runtime environment actually comprises four major components;
- JavaScript engine
- Web APIs
- The Eventloop
- The callback(task) queue
If you want to learn more about JavaScript Runtime Environment and working of JavaScript behind the scenes, Check out my extensive guide on Working of JavaScript under the hood.
Here our main concern is Web APIs. JavaScript runtime environment exposes our code with some extra features provided by the browser client. We can use these features in our application to gain extra power.
Common Asynchronous Web APIs
If have been using JavaScript for a while now, you must be already aware of the popular Web APIs like the DOM API, fetch API, and the Canvas API. You can check the complete list of popular Web APIs here.
Here, we are only interested in asynchronous Web APIs, like the fetch API, setTimeout(), setInterval(), etc.
Consider this example where we are using fetch API to load resources from the server asynchronously. Let's assume our request is gonna take 1 second (1000ms) to return a response.
First I want you to guess the output in the console. If you guessed;
Program Start
→ Function Start
→ Program end
→ 1000ms Delay
→ Fetch call response
→ Function End
You are right. What will be the output of this code, if the fetch API was synchronous? Here.
Program Start
→ Function Start
→ 1000ms Delay
→ Fetch call response
→ Function End
→ Program end
We can clearly see that our fetch call to the server was handled asynchronously because we are using a special fetch Web API. Our program starts, call to the server is initiated. But instead of waiting for the server to return some response, the JavaScript engine moves to the next statements. Even it jumps out of the getPosts
function.
We can use other Web APIs like setTimeout, and setInterval to create timer functions that will run after a specified period. Will run once in the case of setTimeout and periodically in a loop in the case of setInterval. We will not deep dive into them for now.
Callback functions
The callback function is just a normal function that is passed to another function as an argument, and the execution of our callback function is handled inside and by the parent function.
First of all, keep in mind that both the functions involved are just normal JavaScript functions. As functions are also objects like most of the stuff in JavaScript, we can pass them to other functions as arguments.
Our callback function doesn’t get executed right away, but it is gonna sit and wait until parent functions call when it’s needed. This way we can separate some logic in a function and park it outside of the main execution thread. This way we can shift JavaScript from synchronous blocking to asynchronous non-blocking.
Here is a small catch. A callback function can be synchronous or asynchronous. Synchronous callback functions are invoked immediately inside outer functions, but asynchronous functions sit and wait until the specific condition(s) are fulfilled in the future.
Consider this simple example, where we are passing the greeting
function as an argument to processUserInput
function. That is called when the user inputs his/her name in the prompt. Our callback function also accepts an argument (name), which is provided by an external function. Now, you need to guess, whether it is synchronous or asynchronous.
Of course, it is a synchronous callback function. Our callback function is being called immediately in the external function. Well, you might say the user will take some time entering the name, then how it is immediate execution? Well, the answer to this is very simple. The delay is from the user itself. If you see our program, there is no programmatic delay. As soon as there is an input, our program invokes the callback function.
Let's make the above example asynchronous. Now we have added setTimeout Web API, which creates a timer function inside the browser and invokes our callback function after the 2-second mark.
This is an example of the asynchronous callback function because we are parking our callback function using setTimeout Web API. I can prove it by the output of this program. When you run this program, you see;
User {name} input
→ setTimeout() executed
→ Callback function bypassed
→ 2 Second Delay
→ Hello, {name}
In traditional JavaScript, we used to handle our asynchronous logic using callback functions. This is the reason you might see some old StackOverflow answers (I often do need them 😆) using callback functions, and sometimes callback hells.
Callback Hell
As we know, we pass the callback function to another function to complete its execution inside the parent function. What if we do not need our parent function to be called immediately, but inside another function? Well, another function. Is it a problem? Nope, it's totally fine.
But what if we need to pass control of our second parent function to another function, by passing it to another function? Is it a problem now? It looks messy but no problem. I can handle it. Okay!!! fine.
But what if…. Hey hey, stop 🛑. It is a problem. A big problem indeed. Give me the solution.
The solution to this messy callback chaining was introduced in ECMAScript 2015(ES6). JavaScript introduced more descent and performant way to handle asynchronous code in the form of promises.
JavaScript Promises
The callback function approach is really effective in small programs, with up to 2 to 3 levels of chaining. But beyond that code becomes like hell to read.
JavaScript introduced a very cleaner, more performant, and more elegant way to handle asynchronous behavior in our code.
Promises are JavaScript objects that can have one of the 3 possible states, depending upon the outcome of the promise.
- Pending
- Fulfilled
- Rejected
Promises in JavaScript are no different than promises we make in our life. Like real-world promises, JavaScript promises can be kept or rejected. You make a promise for something in the future, let's say you will quit smoking. With time, there are two possible scenarios;
A promise is fulfilled: You successfully stand by your promise and the promise is kept. In the context of JavaScript promise, the promise object returns some data.
A Promise is rejected: You are not able to fulfill your promise and get rid of the promise instead of smoking😍. In the context of JavaScript promise, on rejection, the promise returns some error.
Hopefully, this far you understand the basic soul of Promises in JavaScript.
I was reading the guide on Promises on W3Schools and saw something really explanatory.
“Producing code” is code that can take some time.
“Consuming code” is code that must wait for the result.
A Promise is a JavaScript object that links producing code and consuming code.
Let me show you what this means with examples. You encounter two scenarios while dealing with Promises in this world of JavaScript.
Creating Promise — “Producing code” is code that can take some time.
Let's create a simple promise, that takes 2 seconds to complete and returns some data if resolved and an error if rejected. This is how we implement this;
What is happening here;
Step 1: We create a new promise by using new Promise()
, and assigns it to myBasicPromise
variable.
Step 2: We are faking the time factor by 2 seconds by using setTimeout Web API, to pretend our promise actually took some time to complete.
Step 3: After the 2-second mark, data is available (from an API call, from a local file, etc.).
Step 4: The hard part, we resolve or reject our promise based on performing checks on the data we retrieve.
Keep one thing in mind that, when you are using JavaScript, you will rarely need to create promises, but consume in most cases. All of the latest libraries, modules, and features come with built-in a promise-based approach. Like, fetch API, axios client, File API, etc.
Consuming Promise — “Consuming code” is code that must wait for the result.
Let's consume our promise. Wait. There are two approaches that we can use to consume promises.
then()
— catch()
methodsasync
/ await
keywords
Let's start with the first method.
Using then() catch() methods
What is happening here;
Step 1: If the promise was resolved(fulfilled) after checks we make on data, we get the result inside then()
method. We can do anything with this response data.
Step2: If the promise did not pass checks and was rejected, then()
method is skipped altogether and we catch errors inside catch()
method.
I have added the last console statement so you can see, that this statement will always run before the promise result or error. This shows us that the promise runs asynchronously and doesn't block the preceding code. Well, there is another method attached to the promise object called finally()
. I forgot to add it in the screenshot. You can add it after the catch block, and it will always run when the promise has been settled(resolved or rejected.)
Using async / await keywords
We can consume our promises using async await keywords as well. Here is how.
Step 1: We are wrapping our promise-consuming logic inside an async
function, so we can use await
keyword. Awaiting our promise object returns results if the promise was fulfilled. This step is similar to using then()
method in the above approach.
Step 2: But what if our promise was rejected? How do we catch errors in this case? We can use JavaScript special try{} catch{}
. It will try to execute our promise, but will also catch any error which pops out during the process. Remember that, try{} catch{} blocks are not specific to promises, but they are general JavaScript features that we are using in this approach.
Promise Chaining
Let's discuss the main reason, why we should use Promises over callback functions. Callback chaining(callback hell) was the main reason we adopted Promises, now we need to do the same thing in Promises.
You can easily chain a series of actions simply by using multiple then()
methods. We will discuss the promise-based implementation of fetch API.
Using chaining then() methods
As you can see, we are not producing code here, meaning we are not creating a promise. We are just the consumers, as you will be in most cases.
What is happening here;
Step 1: We are fetching posts from JSON placeholder API. When we call fetch(API_END_POINT)
, it returns us a promise object. From here we can handle our promise by using the then() method.
Step 2 — First then(): We receive a response inside first then() block, when fetch()
promise, was resolved
— successfully fetched. We might encounter an error as well inside the catch() block, due to network problems, or in case the server responds with the error.
But why do we need chaining in this particular case? The reason is that fetch API returns the response object in the first place. But we need data instead. This response object, holds the json()
method inside its prototype(__proto__
) property. We call the json() method on the response object, which in turn returns another promise. Which can be handled inside first then() block, or we can chain then() blocks.
Step 3 — Second then(): Keep in mind that, this particular then() method is not attached to the main fetch() promise. But the promise returned by then() just behind it. Once res.json()
promise resolves, we get our data. We have retrieved our data, so need to further chain promise.
Chaining using async/await methods
I am not going to explain this time. The guide's length has already grown more than I expected. I believe you already know it.
BONUS
As you can see, the fetch()
promise accepts an argument. So let me tell you, how you can pass arguments to your promise.
Simply wrap your promise inside a function, and return the promise from it.
Final Step
To completely understand asynchronous code behavior in JavaScript we need to address the final question.
What are the microTask queue and the Callback queue in JavaScript?
Well, to understand these concepts we need to know little about how the JavaScript engine executes asynchronous code. Whenever Promise, Web API, Mutation Observer, etc. are encountered inside our code. It is added to the Callstack. As we do not intend it to be executed right away, it creates a sort of timer function and pops out of the main execution stack.
Once the timer function has expired, our callback function(function passed to then()
or function passed to setTimeout()
, etc.) is sent to one of the Callback queue or the microTask queue.
Callback functions from Promises, Mutation Observers, and fetch API has higher priority and goes to the microTask queue.
Ordinary callback functions from Web APIs like setTimeout, and setInterval go to the callback queue.
The MicroTask queue always has higher priority, and eventloop will always check for the callback queue once all of the callbacks from the microTask queue have been transferred to the Execution stack already.
You can learn more about the MicroTask queue and the Callback queue from this awesome article at geeksforgeeks.
Wrapping Up
Hopefully, you liked this guide, and let me know if something was confusing in this entire guide. If you find some mistake, please let me know. I would love to make the correction for future readers.
I regularly post guides like this, about the MERN stack. If this is your field of interest and want to read other guides like this, do follow me.
See ya soon. Till then stay safe and try to keep others safe. ❤️