You don’t know JavaScript until you can beat this game
By: Mert Mumtaz
Tags: JavaScript, Software Development, Coding, Programming, Computer Science

The other day I was conducting a routine phone screen for an intermediate developer position and asked the following question as a warm-up:

Me: Is JavaScript synchronous or asynchronous ? Candidate: It’s asynchronous. Me: Are you sure? JavaScript just by itself? Candidate: 100%.

This answer concerned me. It concerned me because I knew that he had around 3 years of experience writing production JavaScript code. He was quite a competent developer and answered most of my questions correctly. Yet, his answer immediately raised doubts in my head. (Note: Some people are saying this is a trick question. To be clear, the goal is not to trick the candidate. The goal is to induce further dialogue about the topic to see what they are familiar with in an open-ended way. The concerning part to me is if someone answers with a one-word definitive Yes/No and no attempt to dive deeper into the why they gave such an answer. I would never disqualify a candidate based solely on their answer here; I’d just like to understand how much they know about some of the nuances of the language).

I don’t think it was his fault. If you just write JavaScript code without knowing a little about its history and inner workings first, you would likely conclude that it’s asynchronous as your default answer. I’d like to suggest that this is wrong. At the very least, it’s misleading.

In my view, JavaScript is a synchronous language and it always has been. The confusion arises when we think of running JavaScript in certain environments (browsers or server-side with Node). This is because these environments give us access to asynchronous functionality, like the classic setTimeout function. It is very important to note that the setTimeout function itself is not a part of native JavaScript but a part of the browser API instead. (Note: you could reason that since Promises are now a part of ECMAScript and since JavaScript is a “dialect” of ECMAScript, then it must also be asynchronous in nature. This is an interesting point but ultimately it’s misleading and thus I won’t cover it here).

This seemingly ambiguous nature of the language often leads to numerous unintended consequences in our everyday code (and job interviews). Even worse, we’re not always aware of these misunderstandings so we unknowingly pass them on as absolute truths to others, magnifying the problem at hand.

As a result, I thought it would be both fun and useful to craft various scenarios examining and further dissecting the complexities that arise as a result of mixing synchronous JavaScript code with additional asynchronous functionality provided by various JavaScript environments.

To make it a little more entertaining, I’ll present and gamify the various situations/scripts as races, and the functions that get run as horses. The objective of the game is to correctly deduce the final standings of the horses in each race. If you get the standings wrong, or just want to further your understanding, I will also supplement with an explanation section.

Disclaimer: I am assuming that you are already somewhat familiar with the event loop. If you are not, I suggest that you check out this talk explaining it much better than I ever could.

Now, let’s get started with a trivial warm-up race.

Warm-up Race

console.log("Horse A");

console.log("Horse B");

console.log("Horse C");

Results

Horse A Horse B Horse C

Explanation

JavaScript is synchronous!

Race 1

setTimeout(() => console.log("Horse A"), 1000)

console.log("Horse B")

Results

Horse B Horse A

Explanation

Ah, the classic introduction to JavaScript. The most common mistake here is thinking that JavaScript will wait for setTimeout to finish before going to the next line. This makes sense. After all, JavaScript IS synchronous. However, recall that setTimeout itself isn’t native to JavaScript. It is a part of the browser API.

What happens under the hood here is that JavaScript will push setTimeout onto the call stack. It will then be popped off the stack and sent off to the browser APIs to do its work in the background. Once 1000ms passes, the work is done and it will now be sent to a data structure called the job/task queue. Once it’s here, a simple (read: magical) mechanism called the event loop will check if the call stack is empty. If it is, then it will take out the first item in the task queue and push it onto the stack, where it will finally be executed. Otherwise, it will keep executing the functions in the call stack. Of course, while Horse A was doing all of this, Horse B was pushed on to the call stack and executed and hence already finished the race.

If you are unfamiliar with these concepts, then that was probably quite a lot to consume at once. Don’t worry, there are countless resources on the internet, much more resourceful than I, that can teach you all about the event loop. Here is another great talk on the topic.

Race 2

setTimeout(() => console.log("Horse A"), 0)

console.log("Horse B")

Results

Horse B Horse A

Explanation

This is a classic interview question. Note that this is the same situation as Race 1 except now the duration of setTimeout is 0ms instead of 1000ms. Most people wrongly assume that since setTimeout will now only take 0ms, its callback will be executed instantly before we even have a chance to move to the next line. What they overlook is the fact that nothing has actually changed in how JavaScript handles these external asynchronous functions. The setTimeout method still gets pushed on to the call stack where it is then sent off to the browser API followed by entering the task queue. While in the task queue, the event loop looks at our call stack and sees that console.log("Horse B") has been pushed on it. Therefore it can’t dequeue Horse A from the task queue (remember, the event loop will only dequeue from the task queue if the call stack is empty) until console.log("Horse B") has already been executed.

Race 3

setTimeout(() => console.log("Horse A"), 0); wait60Seconds(); //some incredibly expensive synchronous computation console.log("Horse B")

Results

Horse B Horse A

Explanation

You may mistakenly assume that since wait60Seconds takes a really long time, then setTimeout will be finished and pushed on to the stack before Horse B gets printed. I hope by now it is very clear that this will never be the case. Synchronous operations always take precedence in JavaScript. The event loop will never dequeue an asynchronous operation while there are any sort of synchronous functions being pushed on the stack, no matter how long it takes.

Race 4

const asyncOperation = fetch('something.com'); asyncOperation.then((resolvedValue) => console.log("Horse A")); console.log("Horse B")

Results

Horse B Horse A

Explanation

This is one that people consistently get wrong. It’s a common misconception that calling .then on a Promise will cause JavaScript to immediately invoke the function inside of the .then function. After all, you clearly have access to resolvedValue before calling console.log("Horse B") , or at least that’s the order in our script. It’s even named such that once you have your Promise object, you can THEN see what’s inside of it. But this is misleading. The .then method should really be renamed to .wheneverThisValueGetsResolved (unfortunately, not as sexy).

Here is what JavaScript is really doing: when we call fetch , we get back a Promise object. This Promise object actually has two hidden properties, value and onFulfilled . When we push fetch on to the call stack, it gets sent off to the browser APIs and does its work in the background, similar to the setTimeout method. While it’s doing this work, all subsequent synchronous operations are performed, like before.

But wait, we explicitly called .then on our Promise before moving onto console.log ! Well…this is where the two hidden properties value and onFulfilled come into play.

When our background operation is finished, it updates the value property of our Promise. When this happens, this value is automatically passed into the onFulfilled property. Okay, but what’s so special about onFulfilled ? The answer is that the function that you passed into the .then method actually gets pushed onto the onFulfilled property of the Promise. In other words, console.log("Horse A") is actually pushed on to the onFulfilled property of our Promise. When the Promise finally resolves, (i.e., we get back our value), THEN the function(s) in onFulfilled gets to be executed. This is why .then should really be named .wheneverThisValueGetsResolved .

TL;DR: the function that you write inside of the .then method only gets executed when the Promise itself is resolved. Not immediately upon calling .then on the Promise.

Race 5

setTimeout(() => console.log("Horse A"), 0); const promise = Promise.resolve();

promise.then(() => console.log("Horse B")); console.log("Horse C")

Results

Horse C Horse B Horse A

Explanation

Wait, what? How did Horse B finish before Horse A !? setTimeout clearly ran before the Promise was called! WTF JavaScript?

Based on my earlier explanations of how JavaScript works, these results must clearly be wrong. Under the hood, we know that setTimeout was pushed on to the task queue, followed by the promise after it (and we know how queues work) so the results should be Horse C, Horse A, and Horse B. Well…this is actually wrong.

What I didn’t mention before is that there is actually another queue called the microtask queue (originally called PromiseJobs ) that specifically manages Promises. And you guessed it: this queue takes priority over the regular task queue (sometimes called the macrotask queue via V8). To be exact, the simplified algorithm looks something like this:

Run whatever is first dequeued from the task queue (macrotask queue) Run all tasks in the microtask queue Wait till another macrotask appears in the task queue Repeat from Step 1

Note that the first macrotask in our task queue is actually the entire script itself. For example:

Step 1: Run the first macrotask from the queue. In this case, the script itself is the macrotask. This looks something like: execute console.log("Horse C") , enqueue setTimeout to the macrotasks queue after 0ms, and enqueue promise to the microtasks queue.

Step 2: Run everything in the microtasks queue. In our case, this is the function in the .then method of promise , namely: () => console.log("Horse B") .

Step 3: See that the first task in the macrotask queue is setTimeout .

Step 4: Repeat Step 1 (run the setTimeout method now dequeued from the task queue)

Race 6

setTimeout(() => console.log("Horse A"), 5) const promise = new Promise((resolve) => {

setTimeout(() => resolve(), 10)

})

promise.then(() => console.log("Horse B")) console.log("Horse C")

Results

Horse C Horse A Horse B

Explanation

If you got this wrong, it’s probably because you remembered that the microtask queue takes precedence over the macrotask queue, i.e., you thought that Horse B would finish before Horse A since Horse B is tied to a Promise. However, note that setTimeout finishes 5ms before the Promise is resolved. This means that when the event loop goes to check the microtask queue (step 2), it will find nothing (for 10ms) and move on to the macrotask queue, which has the setTimeout method on it after 5ms.

The Final Race

setTimeout(() => console.log("Horse A"), 0); const promise = fetch("someUrl") // takes 100ms

promise.then(x => console.log("Horse B")); waitFor200ms();

console.log("Horse C");

Results

Horse C Horse B Horse A

Explanation

If you get this one right, you are truly a God. This is an example that’s inspired by Will Sentance (CEO of CodeSmith and Instructor for JavaScript: The Hard Parts) that I got wrong the first time I saw it.

At first glance, it is very similar to Race 6. The only real difference is the addition of the waitFor200ms function, which theoretically runs on the main thread as a synchronous function in 200ms.

The reason that Horse B finishes before Horse A this time is that while the waitFor200ms function is being executed on the call stack, our fetch call (which takes 100ms) has been resolved and thus pushed onto the microtask queue in the current round of the event loop, i.e., before we even arrive at Step 2 in the algorithm mentioned above. The event loop will then execute all tasks in the microtask queue before moving onto the latest macrotask, leading to the result.

Bonus Race

async function someFn() {

const promise = new Promise((resolve, reject) => {

setTimeout(() => resolve("Horse A"), 1000)

});



let result = await promise;

console.log(result);

} setTimeout(console.log("Horse B"), 0)

someFn();

console.log("Horse C");

Results

Horse B Horse C Horse A

Explanation

Ahh I pulled a quick one on you! Note that the setTimeout method is directly executing console.log("Horse B") instead of taking a callback function (i.e., () => console.log("Horse B") ) . I thought that I would put this on here as I regularly see some developers (myself included) making this minor mistake.

Finally, the async/await is there to switch things up a bit. You may understandably think that we will be “awaiting” for Horse A to finish before logging Horse C but once again, the synchronous code will always get priority.

Conclusion

Congratulations, you made it to the end of my blabbering! If you correctly guessed the outcome of all of the races, then kudos to you, you are a much better developer than I am!

In any case, I hope you learned some things from this post or at least got to practice your JavaScript skills. Thank you for reading!

Enjoyed this article? If so, get more similar content by subscribing to our YouTube channel!