How Does JavaScript Do Async?

Part One: The Call Stack, JavaScript Engines, and Web APIs

Dave Frame
4 min readJul 27, 2020

You’re waiting in line. The cashier rings up a drunk undergrad’s Funyuns a few places ahead of you. But the undergrad has realized he forgot his wallet. He says he’ll be right back and runs out the door. What should happen now? There’s a line of people waiting, but the cashier has already started ringing up this guy’s order. Is he running across the street? Across town? Do we all wait for him to get back? Will he even remember to come back? If we were a synchronous function, we’d have to wait however long it takes.

In Sync

One solution to the problem above might be: go to a different line. If we were at the Ruby store, or Python, or Java, or Haskell, or Elixer, this might be a manager’s solution. These are, after all, multithreaded languages. There are multiple lines to choose from.

However, we are in the blue fluorescent-lit, late-night bodega that is JavaScript. It’s the only place that’s open in Browsertown (except WebAssembly, which still doesn’t have access to the DOM, so, here we are). There is one line. The cashier seems to have mood-conditional aphasia and no problem waiting. That is to say, JavaScript is a single threaded language. At a high level, this means it has one call stack and one memory heap.

NOTE: Languages are not primarily identified as being single threaded or multithreaded (i.e., being single-threaded or multithreaded is a property of how a language is processed rather than a “type” of language). A code-based metaphor might be to say this.isMultithreaded is a method (metaphorically; this is not literally true) rather than SingleThread or Multithread classes extending the parent class of Language.

The Call Stack

The call stack is the data structure that stores and manages the execution contexts of our script. Execution context is something like “the environment (or scope) in which a function exists and operates.” If you’re thinking about scope chain, you’re in the right neighborhood. So, the call stack keeps track of our functions and determines how/when they are executed as well as the information (variables, objects, etc.) to which a function has access.

Stacks vs. Heaps

Call stacks and memory heaps are aptly named to denote their our level of access. Heaps are not strictly organized. That’s ok though, we can grab what we need from a heap when we need it. That’s good for keeping objects and values close, where we might reference them again in no particular order. When we need more structure, like when we want to make sure things happen in a particular order, we can use a stack.

One constraint of a stack is that we only have access to the top. Imagine one of those buffet plate dispensers; you can only get to the plates at the bottom of the stack by removing the plates from the top of the stack. In programming, this is referred to as a LIFO (Last In, First Out) data processing method.

As a consequence, our code must generally proceed in order, with each function awaiting the completion of the preceding block. We can see this in action in the following block:

alert('You clicked me!');
let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
/* source: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing */

Blocking Functions

Javascript will evaluate code line by line. So, when it hits the first line — the alert() — a message will appear. Now, the rest of our code must await the completion of the alert. This is an example of a blocking function. Until the function is completed (we’ve clicked “OK”), we can’t see our newly-added paragraph, or interact with anything else that might be on our page.

Single Thread Async

So we’re dealing with the web. We probably need to get information from other servers and we don’t know how long that’s going to take (returning to the bodega metaphor, we don’t know how long the undergrad will take to get back). We don’t want the line to be held up indefinitely. Thus enters asynchronous Javascript, allowing us to metaphorically skip the line and do more work while we wait.

But wait. If Javascript is single-threaded, you might ask, if it has just one call stack that can only deal with the top of the stack, how can it be asynchronous? The answer: it gets by with a little help from its friends.

It Takes a Browser

For JavaScript to behave asynchronously, it needs some things that only the browser (or Node.js) can offer. Namely, JavaScript engines and Web APIs.

JavaScript engines are the compilers that turn our nice, human-readable script into machine code. They do this using Just-In-Time Compilation, an innovation of Chrome’s V8 engine that has since been picked up by Safari’s Nitro engine and Firefox’s SpiderMonkey. This allows our code to be compiled as necessary at runtime, rather than all at once before runtime.

Web APIs are behind many of the most common functions we write in web applications. The DOM itself comes from a Web API. We might also recognize the ubiquitous Fetch API. This is the first key to unlocking JavaScript’s asynchronous functionality. Web APIs begin a function’s journey in circumnavigating our LIFO call stack. Functions like fetch() are taken off the stack and passed to Web APIs for processing.

So, we understand JavaScript’s constraints. We know the tools it uses to get around those constraints. Now, we’ve shipped off our function to the Web API. How does it get back, though? And what comes with it? These are the questions that I’ll try to answer in Part Two: Callback Queues, Event Loops, and Promises.

--

--

Dave Frame
Dave Frame

Written by Dave Frame

Full Stack Web Developer//MFA in Creative Nonfiction

No responses yet