Asynchronous Javascript

vinay kumar
4 min readFeb 27, 2023

--

Ajax

Why do we need asynchronous programming?

Normally, our JavaScript code is executed line by line, top to bottom. An instruction can only run once the previous one has completed. It is synchronous because there is only one thing happening at the same time.

console.log(1)
console.log(2)

This is great because it’s easy to read and reason about.

The problem occurs when one line of code takes more time. We can simulate a situation like that using window.alert:

window.alert(1)
console.log(2)

alert freezes the browser and 2 won’t log to the console before we close the modal.

What if we have a time consuming operation but we don’t want it to block our app like alert does? We want any instructions following the time-consuming one to be able to run before it finishes. We want the long thing to run in the background and let us know when it finishes. To do that, we need to use an asynchronous function, such as setTimeout:

console.log(1)
console.log(2)
setTimeout(() => console.log('Whoops! I’m late!'), 1000)
console.log(3)

The third line will execute after 1000 milliseconds but the fourth will run immediately, without waiting for the timeout to finish.

If setTimeout wasn’t asynchronous, the fourth line and anything executed after it would have to wait for the timeout to complete. Our entire website would become unresponsive every time we’d set a timeout.

Now, what if we set the second argument to 0?

console.log(1)
setTimeout(() => console.log(2), 0)
console.log(3)

Turns out, 2 will print to the console after 3! What?!

If we have pending synchronous and asynchronous instructions, the synchronous ones will run before the asynchronous one. Because we’re using setTimeout here, which is an asynchronous function, the third line doesn’t wait for 2 to print.

How to structure asynchronous code

We want to log 2 before 3 and after 1, but because setTimeout is asynchronous, 2 always logs last.

We can fix this by moving the third log inside the callback function provided to setTimeout:

console.log(1)
setTimeout(() => {
console.log(2)
console.log(3)
}, 1000)

Now everything works as intended. We leave the first log outside of the function because it doesn’t depend on any other numbers being printed, it is synchronous and it comes before any asynchronous code. That way, even if we set the timeout to 1000 milliseconds, one will still log immediately. We moved the last log into the function body because it can only be printed once 2 is logged to the console. 3 will log to the console right after 2.

Understanding which tasks depend on one another and which ones dont’t is crucial for leveraging asynchronous programming’s power.

The callback pattern

Now, what if we want to log 1.5 asynchronously and it takes 2000 milliseconds? It should log after 1 and before 2. In order to do that, we would have to do something like this:

console.log(1)setTimeout(() => {
console.log(1.5)
setTimeout(() => {
console.log(2)
console.log(3)
}, 1000)
}, 2000)

This is a bit difficult to wrap your head around. What we’re essentially doing is we:

1. Perform a synchronous action (log 1) that runs immediately

2. Then we perform an asynchronous action (log 1.5) that takes two seconds

3. And then, we perform another asynchronous action (log 2 and 3) that depends on the previous one and takes one second. Since it has to wait for 1.5 to be logged, it will be printed after 3 seconds total.

Our code gets nested deeper with every asynchronous operation. This can get difficult to manage as our program grows.

We can mitigate that by refactoring our code into functions:

const logTheRest = () => {
setTimeout(() => {
console.log(2)
console.log(3)
}, 1000)
}const logAsync = callback => {
setTimeout (() => {
console.log(1.5)
callback()
}, 2000)
}console.log(1)
logAsync(logTheRest)

By writing code like this, we avoid deep nesting and make things more readable. We call logAsync, which “calls back” logTheRest once it does its job. We call logTheRest, which is passed as the callback argument, inside of setTimeout, so it only runs once 1.5 is logged. Only logAsync knows when it finishes, so we pass it logTheRest and it decides when to call it.

This is similar to passing an event listener:

document.body.addEventListener('click', () => console.log('hi'))

We provide a function and once the event fires, the browser calls that function. You can read more about how to avoid “callback hell” here.

--

--