Background
I’ve recently had a chance to have an interview with a company and I was asked this question.
“What are synchronous and asynchronous? What are the differences between them?”
My answer to that question was something like this:
“Well, synchronous means the flow of the program is just one. Whereas, asynchronous can be divided into multiple streams.”
Here is the follow-up question that I was asked.
“Ok, then what is async/await? what does it do?”
My answer to the question was something like this:
“Async/await is a syntactic sugar that makes asynchronous behave like synchronous.”
I wasn’t so sure that I answered them correctly, so I decided to take a deeper look after the interview.
Now, let’s take a look together at what I’ve found.
Interestingly enough, a lot of websites and blogs mention that synchronous is single-threaded and asynchronous is multi-threaded. However, it turns out this is not necessarily true.
Synchronous
We hear the term asynchronous quite often, but not synchronous. Why is that?
This is because JavaScript is synchronous by default. We don’t necessarily need to do anything to make JavaScript run as synchronously.
What is synchronous anyways?
Synchronous means dependence. Each task is dependent on others. When a task is executed, another task can’t be executed before the first task is complete. It uses a blocking I/O protocol. This is just a fancy term that is used in the computer world. Again, this just means that the code will be executed one after another.
For easy understanding, imagine you and others are baton-running. You would need to wait until your teammate delivers a baton to you. Once you get the baton, you can start running.
Here is an image for a simple description:
We have 2 lanes (threads) A and B. As you can see runners (tasks) can run on lanes (threads) A and B, however, on each lane (thread) a runner (task) needs to be done for another one to run.
Wait, didn’t I say that JavaScript is single-threaded? How do we make it multi-threaded?
Yes, JavaScript is single-threaded, but there are ways that other brilliant engineers invented to make it act multi-threaded.
Here are 2 ways that you can use to achieve this:
1. If you are using plain JavaScript, we can use Web Workers API
Useful link: How to use Web Workers API
2. If you are using Node.js, we can use the worker-threads module
Useful link: Multi-threading in Node.js
Asynchronous
Then what is asynchronous?
Asynchronous means independence. Each task doesn’t care about others. When a task is executed, another task can be executed while we wait for the first task to be completed. It utilizes advanced technologies of the modern day. If our computers can compute or handle multiple things at the same time, why not take that advantage and use it?
For easy understanding, imagine you and others are running on a running track just for exercising. You don’t need to wait for anyone. You can just run whenever you want to run. This would make them meet their exercise goals much faster 🙂
Here is an image for a simple description:
We have 2 lanes (threads) A and B. As you can see runners (tasks) can run on lanes (threads) A and B, and they are running on their own terms.
Now, let’s take a look at how we can make JavaScript asynchronous.
There are 3 approaches that we can take to do this.
1. Callback
2. Promise
3. Async/Await
Here are some simple examples for each approach. I will make other posts discussing these in detail in the future but for now, I want to show you what they look like.
1. Callback
One thing to be careful of the callback is that callback is synchronous by default. In order to make it asynchronous, we would need to use async functions or Web APIs inside.
For example, the below code is synchronous because there is no async function inside.
function firstRunner() {
console.log(1);
}
function secondRunner(callback) {
console.log(2);
callback();
}
function thirdRunner() {
console.log(3);
}
firstRunner();
secondRunner(thirdRunner);
If we add setTimeout() in the above code, it becomes asynchronous (setTimeout is a built-in async method).
function firstRunner() {
console.log(1);
}
function secondRunner(callback) {
setTimeout(() => {
console.log(2);
callback();
}, 2000);
}
function thirdRunner() {
console.log(3);
}
firstRunner();
secondRunner(thirdRunner);
2. Promise
function displayResult(result) {
console.log(result);
}
let runnerPromise = new Promise(function (resolve, reject) {
let current = 0;
if (current == 0) resolve("current runner is 0");
else reject("current is not 0");
});
runnerPromise.then(
function (value) {
displayResult(value);
},
function (error) {
displayResult(error);
}
);
3. Async/Await
async function getRunner() {
try {
let response = await fetch("http://url-here");
} catch (error) {
alert(error);
}
}
getRunner();
Async/Await
The background of async/await comes from what we call Callback Hell. Callback Hell essentially means there are numerous callbacks one after another. This makes the code difficult to read and looks really messy.
Why would we have Callback Hell to begin with?
Imagine a situation in which we need to grab a runner that runs for today’s race. Then we want to find his/her address as well.
We would need to do something like below:
const result = fetch("http://get-runner-url-here")
.then((response) => response.json())
.then((data) => {
console.log(data);
const runnerId = data.id;
// Get address of the runner now
return fetch(`http://get-runner-address-url-here/${runnerId}`);
})
.then((response) => response.json())
.then((addressData) => {
console.log(addressData);
});
Here is what’s going on from above code.
1. We are fetching a runner data using fetch.
2. We are making the response in JSON format.
3. We are fetching the runner’s address data using another fetch.
4. We are making the response from address fetch in JSON format.
5. We use that address somehow.
This is a very simple example of promise chaining, but imagine we are dealing with much more complicated structures. We would have endless then to get what we need.
Async/Await is here to rescue us from Callback Hell. Instead of chaining many callbacks, we can store the resolved (either fulfilled or rejected) value by using await.
Async/Await is a very interesting JavaScript syntax. They make asynchronous behave like synchronous.
Let’s take a look at how we could achieve that using an example from above.
async function getRunner() {
try {
let response = await fetch("http://get-runner-url-here");
return response;
} catch (error) {
console.log(error);
}
}
async function getRunnerAddress(runnerId) {
try {
let response = await fetch(
`http://get-runner-address-url-here/${runnerId}`
);
return response;
} catch (error) {
console.log(error);
}
}
let runner = await getRunner();
let address = await getRunnerAddress(runner.id);
We can see that, if we use async/await, the code very much looks like how we write regular JavaScript code (synchronous). At the same time, await makes the promise-returning function (fetch in this case) wait until it gets the result back.
If we don’t wait until we get the proper runner id and try to get the runner’s address, it wouldn’t work because our program doesn’t know whose address it needs to grab. This is a possible situation when things run asynchronously. Using async/await gives us more control over the code by preventing this type of issue.
There we go! I hope this would help anyone who is preparing for an interview or having an issue understanding sync, async, and async/await.
Thank you for reading. If you have thoughts on this, be sure to leave a comment.
I also write articles on Medium, so make sure to check them out on medium/@codingnotes.
Happy coding!