Unraveling the Mystery of the Multi-tasking Single Thread

Diana Liao
Nerd For Tech
Published in
6 min readMay 28, 2021

How JavaScript Imitates Concurrency and Makes Promises

several traffic lights hanging on a wire giving mixed signals
Where do all of these functions go anyway?

Around mid-bootcamp, we were introduced to fetch(), our first asynchronous method. I knew it involved something called a Promise, a chain ofthen() , which dealt with the return of promises, and it was all for the sake of allowing other JavaScript code to run in case the network request took a while. Without this feature, the stack could become “blocked” waiting for a longer process to complete and the user would be unable to interact with the website in the meantime. At the time, learning that general pattern to grab JSON data from an API was enough. As I am nearing the end of my final weeks of bootcamp, attempting a project built upon user interaction with asynchronous code everywhere, I found myself needing to understand how the JavaScript call stack gets filled more and more. So how does a single-threaded language like JS manage to simulate concurrent processing?

One stack, with a dedicated staff

JavaScript indeed runs off of a single call stack of truth, and that makes it single-threaded. But there are a lot more helpers behind the scenes! An asynchronous function will send code elsewhere to a place where it will be handled separately outside of the call stack, and eventually be placed in a separate queue. An event loop will wait for the call stack to empty and then take a “message” with an associated function (i.e. the callback function) from the queue and place it on the call stack to then be run.

Photo by Mae Mu on Unsplash

Let me describe a totally plausible scenario (for a dream anyway). Imagine there is a large platter of pancakes at breakfast with your mom. You love pancakes so this is great. She places one on your plate and your job is to eat it. Great! But your mom knows you better and starts placing another pancake on your plate immediately as you finish them, and it might even be piling up. Amid all of this you eye a waffle iron across the way and say “hmm, I’d like a waffle as well”. *poof* Some comforting childhood figure appears, perhaps Patrick Stewart, and he says “I’ll make it so” and gets started on that waffle. After that confusing event, your mother continues to guilt you into eating the rest of the pancakes. Eating all of these takes some time, even in dream land, and the waffle is ready. However, having waffle in between pancakes is just not allowed, so P-Stew hands it off to LeVar Burton who just keeps holding onto that waffle until you’re done with your mega stack-o-pancakes, at which point he is free to place that waffle onto your plate.

In this scenario, eating all of the food (mostly pancakes) is a big function execution context that needs to be completed, your plate is the call stack, making the waffle is the asynchronous function, your eating the waffle is the callback function, Patrick Stewart is an API, and LeVar Burton is the event loop.

Now that I’ve made it more confusing, let’s take a look at its parts!

The call stack is how a JS interpreter (such as your browser) keeps track of the multiple functions it has to run and in which order during the compilation phase. The JS call stack is last-in-first-out (LIFO), meaning you have to work from the top down.

(Side note: But aren’t functions invoked in the order in the code in more of a first-in-first-out pattern? This stack is not to be confused with compiling lines of code in order. The stack contains the function execution context created when a function is invoked. If you think of a function with many other nested inner functions, you can imagine that the inner functions need to resolve before you can continue on with the outer functions until you reach the global context. So don’t worry, the stack isn’t backwards!)

Here is a much better illustration of how the concurrency model works from this blog post written by Alexander Zlatkov:

animation of an example call stack, web API, callback queue, and event loop
Animation from “How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await” by Alexander Zlatkov

When it’s an asynchronous function’s turn in the call stack, it is still dealt with right then and there (and leaves the call stack in a timely manner), but it is actually handled by another API that will return a callback accordingly to wait in a queue. As far as the call stack is concerned, it’s done and over with and is ready for the next function.

Mind your P’s (promises) and Queues

This single queue is not quite the whole story anymore since the introduction of standardized promises and the async/await function construction.

Your average old school asynchronous function like setTimeout or setInterval will get processed in the browser or Node API (it’s not actually a JS function!) which will send a callback function to the queue explained in the general gist/mega breakfast analogy explained above, where it waits for the call stack to empty before it is processed. This occurs even if you specify the delay to be zero milliseconds. Once it’s async, everything else takes precedence:

setTimeout(() => console.log("bye!"), 0) 
console.log("hi")
console.log("how are you?")
console.log("well, I gotta get going.")
/*
console output:
hi
how are you?
well, I gotta get going.
bye!
*/

If you run the above code in your browser console, you’ll see that even though the delay is set to be zero, the callback function in the setTimeout will run after all other synchronous code.

Beginning in ES6, JS also utilizes a separate job queue used by promises. Once functions that return a promise (such as fetch) resolve, they are fast-tracked to the next available slot in the synchronous code. So there are actually two separate lines waiting to go on the call stack rollercoaster, the regular queue and the fast-pass job queue. These priority queue members may also be indicated by the then() method, such as in the familiar pattern of fetch('someUrl').then(response => response.json()).then(data => handleData(data)) . The then() returns a Promise , taking up to two arguments: a callback function upon the success of a promise and one for the failure. The Promise object represents the eventual results (whether successful or not) of an asynchronous operation. It has three states: pending, fulfilled, or rejected. The creation of promises is either built into the function (like fetch() or then()) or you can explicitly create promises with a Promise constructor.

Using async/await syntax will also create promises. It’s syntactic sugar for Promise creation. In general, placing async before your function declaration makes it asynchronous and return a Promise. That’s it! Within the async function, you can also use await which acts as a then() in that it creates another pausing point to wait for the code on the right to resolve before moving on to the next.

async function interruption(){
const message = await "INTERRUPTING".concat(" STARFISH")
console.log(message)
}
setTimeout(() => console.log("bye!"), 0)
interruption()
console.log("hi")
console.log("how are you?")
console.log("well, I gotta get going.")
/*
console output:
hi
how are you?
well, I gotta get going.
INTERRUPTING STARFISH
bye!
*/

In the above snippet example, you can see that even though the first asynchronous function setTimeout is called before the second interruption function, since interruption uses utilizes promises, it was placed in the job queue and beats out the “bye!” from setTimeout.

Thanks for reading this quick post on async functions and how JS manages to seem multithreaded! Let me know if any terminology is off or you have other comments.

I try to include a cat in every post, so:

Promise for tuna, status: unfulfilled

--

--

Diana Liao
Nerd For Tech

Software developer with social justice roots. I love cats, Star Trek, and singing to inspire the downtrodden.