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.
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:
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.
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.
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.
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 await
ing 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.
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:
await
in front of something async results in an unresolved promise, which would make your test result return a false positiveNow that we’ve covered most of the basic scenarios, let’s touch on some more advanced topics regarding 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:
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.
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.
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 👈.
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 see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.