The Startup
Published in

The Startup

Intro to Asynchronous JavaScript

Three washing machines in a laundry
Photo by Tina Bosse on Unsplash

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.

Asynchronous JavaScript in practice

If this makes little sense to you, that’s fine. Once you start writing asynchronous code yourself, you will start to grasp this concept. And I’m not talking about nesting setTimeout inside of another setTimeout (You can play around with it if you want though). Check out my article about making network requests in JavaScript and see how you can use async JavaScript to get random pictures of dogs on your webpage! Build cool things and learn by doing. Have fun!

“One must learn by doing the thing; for though you think you know it, you have no certainty, until you try.” — Sophocles

If you want to dive deeper into how JavaScript handles asynchronicity, check out this great article about the event loop.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tomasz Chmielnicki

Tomasz Chmielnicki

Web developer, passionate about creating.