Eden Adler Pre-med → programmer 👩🏻‍💻 | Software Engineer @Lemonade_Inc 🍋 | @_what_the_tech 💻 | @WomenStartupNtn 💥 | University of Michigan alumna 〽️

The visual learner’s guide to async JS

5 min read 1546

Have you ever watched or read hours’ worth of tutorials but were still left confused? That’s how I felt when I first dove into learning asynchronous JavaScript. I struggled to clearly see the differences between promises and async/await, especially because under the hood, they’re the same.

Async JS has evolved a lot over the years. Tutorials are great, but they often give you a snapshot of what is the “right” way to do things at that particular point in time. Not realizing I should pay attention to the content’s date (😅), I found myself mixing different syntaxes together. Even when I tried to only consume the most recent content, something was still missing.

I realized much of the material out there wasn’t speaking to my learning style. I’m a visual learner, so in order to make sense of all the different async methods, I needed to organize it all together in a way that spoke to my visual style. Here I’ll walk you through the questions I had about async and how I differentiated promises and async/await through examples and analogies.

Why do we need async?

At its core, JavaScript is a synchronous, blocking, single-threaded language. If those words don’t mean much to you, this visual helped me better understand how asynchronous JS can be more time-efficient:

Thick lines = time the program spends running normally. Thin lines = time spent waiting for the network.

We want to use async methods for things that can happen in the background. You wouldn’t want your entire app to wait while you query something from the database or make an API request. In real life, that would be the equivalent of not being able to do anything — no phone calls, no eating, no going to the bathroom — until the laundry machine is done. This is less than ideal.

Out of the box, JS is synchronous, but we have ways of making it behave asynchronously.

Evolution of async

When searching online for “async JS,” I came across many different implementations: callbacks, promises, and async/await. It was important for me to be clear about each method and its unique value proposition so I could code with consistent syntax throughout. Here’s a breakdown of each one:


Before ES6, we’d implement this async behavior using callbacks. I won’t get too deep into it here, but, in short, a callback is a function that you send as a parameter to another function that will be executed once the current function is finished executing. Let’s just say there’s a reason why people refer to it as “callback hell.”

In order to control the sequence of events, using callbacks, you’d have to nest functions within callbacks of other functions to ensure they occur in the order you expect.

“Callback hell.”

Since implementing this gave us all headaches, the JS community came up with the promise object.


As humans, it’s easier for us to understand and read synchronous code, so promises were created to look more synchronous but act asynchronously. Ideally, it would look something like this:

This might look nice, but it’s missing a few key elements, one of which is error handling. Have you ever gotten an unhandledPromiseRejection error or warning? This is because some error occurred, which caused the promise to be rejected instead of resolved.

In the snippet above, we only handle the case of “success,” meaning that an unhandled promise is never settled, and the memory it is taking up is never freed. If you’re not careful, a promise will silently fail, unless manually handled with catch:


This is the syntactic sugar on top of promises, which helps the code look more readable. When we add the async keyword in front of the function, it changes its nature.

An async function will return a value inside of a promise. In order to access that value, we need to either .then() the method or await it.

Style and conventions aside, it is technically OK to use different async methods together in your code since they all implement async behavior. But once you fully understand the differences between each one, you’ll be able to write with consistent syntax without hesitation.

Since async/await utilizes promises, I initially struggled to separate the two methods in terms of syntax and conventions. To clear up the differences between them, I mapped out each method and its syntax for each use case.

Comparing promises and async/await

These comparisons are a visually upgraded version of what I originally mapped out for myself. Promises are on the left, async/await on the right.


getJSON() is a function that returns a promise. For promises, in order to resolve the promise, we need to .then() or .catch() it. Another way to resolve the promise is by awaiting it.

N.B., await can only be called inside of an async function. The async function here was omitted to show a more direct comparison of the two methods.


Both of these will return Promise {<resolved>: "hi"}. With async, even if you don’t explicitly return a promise, it will ensure your code is passed through a promise.

resolve() is one of the executor functions for promises. When called, it returns a promise object resolved with the value. In order to directly compare this behavior, the async method is wrapped in an immediately invoked function.

More great articles from LogRocket:

Error handling

There’s a few ways to catch errors. One is by using then/catch, and the other is by using try/catch. Both ways can be used interchangeably with promises and async/await, but these seem to be the most commonly used conventions for each, respectively.

A major advantage of using async/await is in the error stack trace. With promises, once B resolves, we no longer have the context for A in the stack trace. So, if B or C throw an exception, we no longer know A’s context.

With async/await, however, A is suspended while waiting for B to resolve. So, if B or C throw an exception, we know in the stack trace that the error came from A.


I’m using single letters for names here to help you more clearly see the differences between the syntaxes. Before, I would read through code samples where I felt like I had to whack through the weeds of the function names to understand what was happening. It became very distracting to me, especially as such a visual learner.

N.B., even though each task is async, these both won’t run the tasks concurrently. I’ll touch on this in Parallel execution below.


There are subtle but important differences here. Remember that async functions return promises, so similarly, if you are using regular promises, you must return them.

Other things to note:

  • Not putting await in front of something async results in an unresolved promise, which would make your test result return a false positive
  • If you want to stub an async method that returns a promise, you can do something like this:

Now that we’ve covered most of the basic scenarios, let’s touch on some more advanced topics regarding async.

Parallel vs. sequential async

Since async/await makes the syntax so readable, it can get confusing to tell when things are executed in parallel versus sequentially. Here are the differences:

Parallel execution

Let’s say you have a long to-do list for the day: pick up the mail, do laundry, and respond to emails. Since none of these things depend on one another, you can use Promise.all() to run each of these tasks. Promise.all() takes an array (for any iterable) of promises and resolves once all of the async methods resolve, or rejects when one of them rejects.

Sequential execution

Alternatively, if you have tasks that are dependent on one another, you can execute them in sequence. For example, let’s say you’re doing laundry. You have to do things in a sequence: wash, dry, fold. You cannot do all three at the same time. Since there’s an order to it, you would do it this way:

These functions are executed in sequence because the return values here are used as inputs for the next functions. So the function must wait until the value is returned in order to proceed executing.

Tip for success

Everyone has a different learning style. No matter how many tutorials I watched or blog posts I read, there were still holes in my async knowledge. Only when I sat down and mapped everything out did I finally put the pieces together.

Don’t get frustrated or discouraged when you come across a concept you struggle with. It’s simply because the information isn’t being presented to you in a way that speaks to your learning style. If the material isn’t out there for you, create it yourself and share it! It might surprise you how many people out there are feeling the same way as you.

Thanks for reading 🙌! Would love to hear your thoughts, feel free to leave a comment.

Connect with me on Instagram and check out my website 👈.

: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!


Eden Adler Pre-med → programmer 👩🏻‍💻 | Software Engineer @Lemonade_Inc 🍋 | @_what_the_tech 💻 | @WomenStartupNtn 💥 | University of Michigan alumna 〽️

Leave a Reply