This Small Corner

Eric Koyanagi's tech blog. At least it's free!

How Exactly do Async Await and Promises Work in JavaScript?

By Eric Koyanagi
Posted on

How does async / await actually work? 

There’s a lot of myth and lack of detail when it comes to this topic. You’ll often read descriptions like “async functions return a promise” and “await makes a function pause”, but there’s sadly still some confusion among developers about what this really does. 

Let’s look into how async/await actually works. The details are very similar between client side JS and server side (NodeJS), so we’ll be discussing both. The only major difference is understanding that server-side Node uses C++ APIs (as we talked about here), while the client side uses browser web APIs. 


History: Those That Don’t Know It Are Doomed To Something

Everyone’s favorite section: looking back in time to figure out how we got here. Async/await wasn’t a part of JavaScript until 2017, but of course the idea goes back much further. For example, C# introduced async/await syntax as early as 2011/2012. 

In general, async/await is similar to the idea of a coroutine, a term coined back in the 1950s (big surprise) by computer scientist Melvin Conway. In other words, the idea of asynchronous program execution is as old as the ideas behind neural networks

Like most concepts in computing, asynchronous programming was an evolution driven by more than one person across decades. It’s not really fair to attribute any one person as the “inventor” of a given async concept. 

JavaScript obviously wasn’t the first to the party, here! Before we can get into the event loop and the details of async/await, we have to take a step back and look at one key underlying structure: the promise. 

To be clear, there’s a slew of concepts that predate the promise: “thunks” from the 60s, “futures” from the 70s, Joule’s “channels” in the 90s, the first true non-blocking implementation in the late 90s (from E lang), to Twisted / Differed in the early 2000s. 

It wasn’t until the early 2000s that the idea of promises was rebirthed in the context of web development especially, as there was a great need for more responsive UIs. 

After being implemented in Java and the Microsoft C# stack, it shouldn’t be a surprise that they came to JavaScript via EMC7. Most languages now support promises of some sort, even PHP has the React/Promise library, although it's unfortunate how the language lags behind the pack in general when it comes to asynchronous programming models. 


Promises in JavaScript

Different sources will point to different points in the timeline to credit the “inventor” of promises as we understand them today. Some point to Dojo framework’s 2006 implementation or Ryan Dalh’s 2009 Node debut or 2012’s introduction of promises in ES6. 

It’s a bit annoying how various online sources reduce this history to a few lines, falsely giving credit to singular individuals. 

I think it’s important especially for younger developers to see the wider context of development and shatter a persistent myth in computer science that great leaps in progress are driven solely by brilliant individuals. That’s just not how these things work!  

It’s also good to respect early pioneers in the field. Even without understanding the scale of modern workloads, they knew that asynchronous computing would be necessary. Many concepts pioneered in the 50s and 60s are more relevant today than when they were first conceived! 

Want a more complete explanation of this history? You should check out Sam Saccone’s great article on this exact topic here. They go into more depth about the history, underscoring the wider context of development that pressed this idea into the mainstream. 

This history is great, but how do promises actually work? The anatomy of a promise is very simple. 

Per the specification, a promise is an object used as a placeholder for an actual value. It can be in only one of three states: fulfilled, rejected, and pending. If the promise enqueues a Job, it’s either fulfilled or rejected based on what Job it queues via the “then” statement. If it hasn’t enqueued a job, the promise is still “pending”. 

Within the promise, either “resolve” or “reject” is called to complete the asynchronous operation. A very simple example might be something like this: 

const examplePromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Success!');
  }, 100);
});

But we probably already know this sort of information…we’ve used promises before, right? How does it actually keep track of which operations are “ongoing”? 


The Event Loop

The Event loop is an important concept for many languages that rely on asynchronous logic. Both Node and client side JS need the event loop for asynchronous flow (like those found in promises) to work. 

Remember how a promise is resolved? It must queue a Job, either a success or failure -- until it does, it’s “pending”. The event loop is what helps us manage all the callbacks registered (promises are just more structured callbacks in the end). 

You can imagine it as an infinite while loop that constantly checks the job queue, executing “then” or “reject” handlers in the order they were added to the queue. Yes, it's as basic as jobs being added to a list and having a loop check that list.

The key element here is that the job queue allows the system to offload async operations and “complete them later”. The event loop will keep track of every async flow, maintain job order, and ensure that the main thread remains unblocked. 

We don’t really need to do a deeper dive into the event loop source code (maybe we will in another article!) to understand what it does and why. Ultimately, promises are structures that make working with callbacks easier. 


Finally, async/await! It’s Just Promises, Though…?

People often say that async/await is “syntactic sugar” around promises. I love the phrase “syntactic sugar” and can’t wait for a name-brand candy with that moniker. In case you don’t know or can’t inuit the meaning from the phrase, this refers to “sweet stuff” you can do with code that doesn’t actually offer new functionality. 

One example is a “for” loop. It’s the same damn thing as a “while” loop, only written in a way that’s more convenient for many use cases. In other words, async/await is nothing more than a “shortcut” we use in code to make life easier. It isn’t actually anything functional in the language itself. 

In yet more other words, async/await is just a wrapper for promises. The point of async/await is not to eliminate the need for promises, though. You might need both in some use cases, because promises offer more granularity. 

That makes sense, right? Async/await is meant to be a shortcut, and sometimes shortcuts aren’t what you really want. I think there’s some pervasive misunderstanding about this idea and a desire to throw almost everything into an async/await pattern…but life is not so simple.

Here’s a very common example that illustrates how you can’t just replace everything with async/await:

Let’s say we want to fetch data from several APIs -- we need to wait until all API operations are done, but we want this to happen as quickly as possible. We know we can call out to each API asynchronously. Great! 

However, if we throw this into an async function and use “await” after each API fetch, that’s not what we want. That’s synchronous, with each API call waiting for the results of the last before fetching. Gross. 

Instead, we should use promises.all to fetch from all three APIs at the same time. This way, we can easily wait for that to “resolve” (meaning all three APIs have resolved) and can more easily handle failure from any one of the underlying calls. 

As we can see, this is a case where we need the granularity that a promise provides and using the syntactic sweet stuff of async/await becomes counterproductive. 

I’ve noted this elsewhere before, but one thing to remember is that JavaScript is single-threaded by design. Making code more concurrent and asynchronous doesn’t necessarily mean that you’re free from all blocking operations. It doesn’t mean that the code is “mutlti-threaded” or even that concurrent code is “running at the same time” as the main thread.


Conclusion

I think being aware of how async/await works compared to promises is very important for building performance applications in JavaScript, be it in NodeJS or client side. For many people reading this, the information is very basic. 

However….I don’t believe that every JavaScript dev has as complete an understanding about asynchronous programming as we probably should. Many devs use these concepts without having a full context for how they developed or why they are used. 

This is one issue I sometimes have with modern technical articles. The focus is on how to do some specific thing because those are the keywords that rank well in Google. The result is a lot of surface information about how to use things without always having the context about why these things exist or what tradeoffs exist in using them. 

You really have to ask Google very specific questions to unravel a broader context that I wish was more heavily rewarded by the algorithms. But hey, that’s a discussion for some other time and place. 


« Back to Article List
Written By
Eric Koyanagi

I've been a software engineer for over 15 years, working in both startups and established companies in a range of industries from manufacturing to adtech to e-commerce. Although I love making software, I also enjoy playing video games (especially with my husband) and writing articles.

Article Home | My Portfolio | My LinkedIn
© All Rights Reserved