DEV Community

Cover image for ⭐️🎀 JavaScript Visualized: Promises & Async/Await
Lydia Hallie
Lydia Hallie

Posted on • Updated on

⭐️🎀 JavaScript Visualized: Promises & Async/Await

Ever had to deal with JS code that just... didn't run the way you expected it to? Maybe it seemed like functions got executed at random, unpredictable times, or the execution got delayed. There's a chance you were dealing with a cool new feature that ES6 introduced: Promises!

My curiosity from many years ago has paid off and my sleepless nights have once again given me the time to make some animations. Time to talk about Promises: why would you use them, how do they work "under the hood", and how can we write them in the most modern way?

If you haven't read my previous post on the JavaScript Event Loop yet, it may be useful to read that first! I'll be covering the event loop again assuming some basic knowledge about the call stack, Web API and the queue, but this time we'll also be covering some exciting extra features 🤩


If you're already somewhat familiar with promises, here are some shortcuts to save you some precious scrolling time.



Introduction

When writing JavaScript, we often have to deal with tasks that rely on other tasks! Let's say that we want to get an image, compress it, apply a filter, and save it 📸

The very first thing we need to do, is get the image that we want to edit. A getImage function can take care of this! Only once that image has been loaded successfully, we can pass that value to a resizeImage function. When the image has been resized successfully, we want to apply a filter to the image in the applyFilter function. After the image has been compressed and we've added a filter, we want to save the image and let the user know that everything worked correctly! 🥳

In the end, we'll end up with something like this:

Hmm... Notice anything here? Although it's... fine, it's not great. We end up with many nested callback functions that are dependent on the previous callback function. This is often referred to as a callback hell, as we end up with tons of nested callback functions that make the code quite difficult to read!

Luckily, we now got something called promises to help us out! Let's take a look at what promises are, and how they can help us in situations like these! 😃


Promise Syntax

ES6 introduced Promises. In many tutorials, you'll read something like:

"A promise is a placeholder for a value that can either resolve or reject at some time in the future"

Yeah... That explanation never made things clearer for me. In fact it only made me feel like a Promise was a weird, vague, unpredictable piece of magic. So let's look at what promises really are.

We can create a promise, using a Promise constructor that receives a callback. Okay cool, let's try it out!

Alt Text

Wait woah, what just got returned?

A Promise is an object that contains a status, ([[PromiseStatus]]) and a value ([[PromiseValue]]). In the above example, you can see that the value of [[PromiseStatus]] is "pending", and the value of the promise is undefined.

Don't worry - you'll never have to interact with this object, you can't even access the [[PromiseStatus]] and [[PromiseValue]] properties! However, the values of these properties are important when working with promises.


The value of the PromiseStatus, the state, can be one of three values:

  • fulfilled: The promise has been resolved. Everything went fine, no errors occurred within the promise 🥳
  • rejected : The promise has been rejected. Argh, something went wrong..
  • pending: The promise has neither resolved nor rejected (yet), the promise is still pending.

Alright this all sounds great, but when is a promise status "pending", "fulfilled" or "rejected"? And why does that status even matter?

In the above example, we just passed the simple callback function () => {} to the Promise constructor. However, this callback function actually receives two arguments. The value of the first argument, often called resolve or res, is the method to be called when the Promise should resolve. The value of the second argument, often called reject or rej, is the value method to be called when the Promise should reject, something went wrong.

Let's try and see that gets logged when we invoke either the resolve or reject method! In my example, I called the resolve method res, and the reject method rej.

Awesome! We finally know how to get rid of the "pending" status and the undefined value! The status of a promise is "fulfilled" if we invoked the resolve method, and the status of the promise is "rejected" if we invoked the rejected method.

The value of a promise, the value of [[PromiseValue]], is the value that we pass to the either the resolved or rejected method as their argument.

Fun fact, I let Jake Archibald proofread this article and he actually pointed out there's a bug in Chrome that currently shows the status as "resolved" instead of "fulfilled". Thanks to Mathias Bynens it's now fixed in Canary! 🥳🕺🏼



Okay so, now we know a little bit better how to control that vague Promise object. But what is it used for?

In the introductory section, I showed an example in which we get an image, compress it, apply a filer, and save it! Eventually, this ended up being a nested callback mess.

Luckily, Promises can help us fix this! First, let's rewrite the entire code block, so that each function returns a Promise instead.

If the image is loaded and everything went fine, let's resolve the promise with the loaded image! Else, if there was an error somewhere while loading the file, let's reject the promise with the error that occurred.

Let's see what happens when we run this in the terminal!

Cool! A promise got returned with the value of the parsed data, just like we expected.

But... what now? We don't care about that entire promise object, we only care about the value of the data! Luckily, there are built-in methods to get a promise's value. To a promise, we can attach 3 methods:

  • .then(): Gets called after a promise resolved.
  • .catch(): Gets called after a promise rejected.
  • .finally(): Always gets called, whether the promise resolved or rejected.

The .then method receives the value passed to the resolve method.

The .catch method receives the value passed to the rejected method

Finally, we have the value that got resolved by the promise without having that entire promise object! We can now do whatever we want with this value.


FYI, when you know that a promise will always resolve or always reject, you can write Promise.resolve or Promise.reject , with the value you want to reject or resolve the promise with!

Alt Text

You'll often see this syntax in the following examples 😄


In the getImage example, we ended up having to nest multiple callbacks in order to run them. Luckily, the .then handlers can help us with that! 🥳

The result of the .then itself is a promise value. This means that we can chain as many .thens as we want: the result of the previous then callback will be passed as an argument to the next then callback!

In the case of the getImage example, we can chain multiple then callbacks in order to pass the processed image onto the next function! Instead of ending up with many nested callbacks, we get a clean then chain.

Perfect! This syntax already looks way better than the nested callbacks.


Microtasks and (Macro)tasks

Okay so we know a little better how to create a promise and how to extract values out of a promise. Let's add some more code to the script, and run it again:

Wait what?! 🤯

First, Start! got logged. Okay we could've seen that one coming: console.log('Start!') is on the very first line! However, the second value that got logged was End!, and not the value of the resolved promise! Only after End! was logged, the value of the promise got logged. What's going on here?

We've finally seen the true power of promises! 🚀 Although JavaScript is single-threaded, we can add asynchronous behavior using a Promise!


But wait, haven't we seen that before? 🤔 In the JavaScript event loop, can't we also use methods native to the browser such as setTimeout to create some sort of asynchronous behavior?

Yes! However, within the Event Loop, there are actually two types of queues: the (macro)task queue (or just called the task queue), and the microtask queue. The (macro)task queue is for (macro)tasks and the microtask queue is for microtasks.

So what's a (macro)task and what's a microtask? Although there are a few more than I'll cover here, the most common are shown in the table below!

(Macro)task setTimeout | setInterval | setImmediate
Microtask process.nextTick | Promise callback | queueMicrotask

Ahh, we see Promise in the microtask list! 😃 When a Promise resolves and calls its then(), catch() or finally(), method, the callback within the method gets added to the microtask queue! This means that the callback within the then(), catch() or finally() method isn't executed immediately, essentially adding some async behavior to our JavaScript code!

So when is a then(), catch() or finally() callback executed? The event loop gives a different priority to the tasks:

  1. All functions in that are currently in the call stack get executed. When they returned a value, they get popped off the stack.
  2. When the call stack is empty, all queued up microtasks are popped onto the callstack one by one, and get executed! (Microtasks themselves can also return new microtasks, effectively creating an infinite microtask loop 😬)
  3. If both the call stack and microtask queue are empty, the event loop checks if there are tasks left on the (macro)task queue. The tasks get popped onto the callstack, executed, and popped off!

Let's take a look at a quick example, simply using:

  • Task1: a function that's added to the call stack immediately, for example by invoking it instantly in our code.
  • Task2, Task3, Task4: microtasks, for example a promise then callback, or a task added with queueMicrotask.
  • Task5, Task6: a (macro)task, for example a setTimeout or setImmediate callback

First, Task1 returned a value and got popped off the call stack. Then, the engine checked for tasks queued in the microtask queue. Once all the tasks were put on the call stack and eventually popped off, the engine checked for tasks on the (macro)task queue, which got popped onto the call stack, and popped off when they returned a value.

Okay okay enough pink boxes. Let's use it with some real code!

In this code, we have the macro task setTimeout, and the microtask promise then() callback. Once the engine reaches the line of the setTimeout function. Let's run this code step-by-step, and see what gets logged!


Quick FYI - in the following examples I'm showing methods like console.log, setTimeout and Promise.resolve being added to the call stack. They're internal methods and actually don't appear in stack traces - so don't worry if you're using the debugger and you don't see them anywhere! It just makes explaining this concept easier without adding a bunch of boilerplate code 🙂

On the first line, the engine encounters the console.log() method. It gets added to the call stack, after which it logs the value Start! to the console. The method gets popped off the call stack, and the engine continues.

The engine encounters the setTimeout method, which gets popped on to the call stack. The setTimeout method is native to the browser: its callback function (() => console.log('In timeout')) will get added to the Web API, until the timer is done. Although we provided the value 0 for the timer, the call back still gets pushed to the Web API first, after which it gets added to the (macro)task queue: setTimeout is a macro task!


The engine encounters the Promise.resolve() method. The Promise.resolve() method gets added to the call stack, after which is resolves with the value Promise!. Its then callback function gets added to the microtask queue.


The engine encounters the console.log() method. It gets added to the call stack immediately, after which it logs the value End! to the console, gets popped off the call stack, and the engine continues.

The engine sees the callstack is empty now. Since the call stack is empty, it's going to check whether there are queued tasks in the microtask queue! And yes there are, the promise then callback is waiting for its turn! It gets popped onto the call stack, after which it logs the resolved value of the promise: the string Promise!in this case.

The engine sees the call stack is empty, so it's going to check the microtask queue once again to see if tasks are queued. Nope, the microtask queue is all empty.

It's time to check the (macro)task queue: the setTimeout callback is still waiting there! The setTimeout callback gets popped on to the callstack. The callback function returns the console.log method, which logs the string "In timeout!". The setTimeout callback get popped off the callstack.

Finally, all done! 🥳 It seems like the output we saw earlier wasn't so unexpected after all.


Async/Await

ES7 introduced a new way to add async behavior in JavaScript and make working with promises easier! With the introduction of the async and await keywords, we can create async functions which implicitly return a promise. But.. how can we do that? 😮

Previously, we saw that we can explicitly create promises using the Promise object, whether it was by typing new Promise(() => {}), Promise.resolve, or Promise.reject.

Instead of explicitly using the Promise object, we can now create asynchronous functions that implicitly return an object! This means that we no longer have to write any Promise object ourselves.

Although the fact that async functions implicitly return promises is pretty great, the real power of async functions can be seen when using the await keyword! With the await keyword, we can suspend the asynchronous function while we wait for the awaited value return a resolved promise. If we want to get the value of this resolved promise, like we previously did with the then() callback, we can assign variables to the awaited promise value!

So, we can suspend an async function? Okay great but.. what does that even mean?

Let's see what happens when we run the following block of code:

Alt Text

Hmm.. What's happening here?


Alt Text

First, the engine encounters a console.log. It gets popped onto the call stack, after which Before function! gets logged.


Alt Text

Then, we invoke the async function myFunc(), after which the function body of myFunc runs. On the very first line within the function body, we call another console.log, this time with the string In function!. The console.log gets added to the call stack, logs the value, and gets popped off.


Alt Text

The function body keeps on being executed, which gets us to the second line. Finally, we see an await keyword! 🎉

The first thing that happens is that the value that gets awaited gets executed: the function one in this case. It gets popped onto the call stack, and eventually returns a resolved promise. Once the promise has resolved and one returned a value, the engine encounters the await keyword.

When encountering an await keyword, the async function gets suspended. ✋🏼 The execution of the function body gets paused, and the rest of the async function gets run in a microtask instead of a regular task!


Alt Text

Now that the async function myFunc is suspended as it encountered the await keyword, the engine jumps out of the async function and continues executing the code in the execution context in which the async function got called: the global execution context in this case! 🏃🏽‍♀️


Alt Text

Finally, there are no more tasks to run in the global execution context! The event loop checks to see if there are any microtasks queued up: and there are! The async myFunc function is queued up after resolving the valued of one. myFunc gets popped back onto the call stack, and continues running where it previously left off.

The variable res finally gets its value, namely the value of the resolved promise that one returned! We invoke console.log with the value of res: the string One! in this case. One! gets logged to the console and gets popped off the call stack! 😊

Finally, all done! Did you notice how async functions are different compared to a promise then? The await keyword suspends the async function, whereas the Promise body would've kept on being executed if we would've used then!


Hm that was quite a lot of information! 🤯 No worries at all if you still feel a bit overwhelmed when working with Promises, I personally feel that it just takes experience to notice patterns and feel confident when working with asynchronous JavaScript.

However, I hope that the "unexpected" or "unpredictable" behavior that you might encounter when working with async JavaScript makes a bit more sense now!

And as always, feel free to reach out to me! 😊

Twitter 👩🏽‍💻 Instagram 💻 GitHub 💡 LinkedIn 📷 YouTube 💌 Email

If you want to know more about promises states (and fates!), this Github repo does an excellent job explaining the differences.

GitHub logo domenic / promises-unwrapping

The ES6 promises spec, as per September 2013 TC39 meeting

Top comments (163)

Collapse
 
spoike profile image
Mikael Brassman • Edited

Loving the article and the visualizations!

There is a gotcha however, setTimeout(..., 0) will not immediately put the task to the macrotask queue because of browser throttling. While the result will be the same, the reason why setTimeout fires last is because it is queued up in the macro task queue "a lot" later (4ms) rather than it being a macro task.

That setTimeout is throttled is why Node environment has setImmediate. For more information about browser throttling: developer.mozilla.org/en-US/docs/W...

Collapse
 
vagoel profile image
Varun

Extremely useful and intuitive.
Love the way you describe problem and provide innovative graphical solutions.
Thanks for creating wonderful visuals.

Just wondering what tools you use for creating those. 🧐

Collapse
 
vinstar profile image
Vincent Touquet

Great. I think I understand a lot more now :) I was wondering, how did you make this image? dev-to-uploads.s3.amazonaws.com/i/... I would love to have a node REPL which also shows me the "green" stuff...

Collapse
 
lydiahallie profile image
Lydia Hallie

Haha I just make it manually, sorry! It's a pain to find a repl that shows that..

Collapse
 
conermurphy profile image
Coner Murphy

If you wouldn't mind, what software do you use to make it? I've been trying to figure it out for a while and came up blank. Thanks love the work.

Thread Thread
 
qm3ster profile image
Mihail Malo

I too wish to know this 👌

Thread Thread
 
ziizium profile image
Habdul Hazeez
Thread Thread
 
qm3ster profile image
Collapse
 
adgower profile image
Alex Gower

Those are dope.

Collapse
 
selvarajrch profile image

Hi Lydia,

Thanks for writing this up. Great article, enjoyed it and nice animations!!
IMHO, this article is missing one bit and it would be perfect if you can update for the below change.

Now that the async function myFunc is suspended as it encountered the await keyword, the engine jumps out of the async function and continues executing the code in the execution context in which the async function got called: the global execution context in this case!

After this statement, the article can mention this is where the statement console.log('After function') gets executed printing After function to the console.

Thanks.

Collapse
 
charanweb profile image
CHARAN

I got a difference answer when i tried the last code:
Async/await executing the next lines but saving in memory not showing in the output ,until await got the result. And its working synchronously. it shows first await result and executing line by line.


const x = Promise.resolve("Async function")

async function getme(){
 console.log("Before Async")
 const res = await x
 console.log(res)
 console.log("After Async")
}
getme()
//results
 Before Async
 Async function
 After Async
Promise {<fulfilled>: undefined}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vitruvius21 profile image
Vitruvius • Edited

You place all console.logs in async function not outside, thus after "Before Async" it sees await, resolves promise and moves the getme() function into the microtasks, since there are no other tasks in callstack, getme() moves back to callstack and executes where it stopped i.e. at await, so res gets initialized, then 'Async function' and 'After Async' are logged.

Collapse
 
silverbullet069 profile image
VH • Edited

This is interesting, you use Promise.resolve function to return a Promise directly, instead of using a callback function that return that Promise.resolve function. This way, you have resolved your Promise right in line 1, not in line const res = await x.

Collapse
 
bearfinn profile image
Kritsada Sunthornwutthikrai

Just a minor suggestion - the visualization says "Timeout!", but the content says. "In timeout!"
Otherwise, it was a very nice article. I know more about the JavaScript queues!

I have a bit of a question. From what I understood, async functions are there to solve the problems where there are functions that took a long time to process such as API calls. However, if the call stack waits for the Promise to resolve, then encounter the await keyword, it means that the execution time will still be similar. Is there any part I misunderstand?

Collapse
 
voanhcuoc profile image
Khoa Che

Only the async functions encountering await will be suspended, other functions - that don't depend on the awaited result - still run.

Collapse
 
martincomito profile image
Martín Comito • Edited

Thanks A LOT for this post, I been waiting a long time for such a clear explanation of this topic

Collapse
 
lydiahallie profile image
Lydia Hallie

Thank you so much!

Collapse
 
jvor97 profile image
jvor97

Great article, thanks !
I have a question: What would happen if we use an actual http request, e.g. getImage().then(res => console.log('res1')).then(res => console.log('res2'))). getImage body would go into webapi, then into (micro?macro?) queue, and back to callStack where .then would be executed, sent to micro queue and the same process with the next .then...?

Collapse
 
lesha profile image
lesha 🟨⬛️

This means that we no longer have to write any Promise object ourselves.

I'd just like to interject for a moment. You still need to write Promises when you actually implement these async functions. Almost all browser and Node APIs are callback-based, not even promise-based so you either need a thin wrapper library that converts them to Promises or do it yourself.

What you don't need anymore is .then chain because that's the whole point of async syntax

Collapse
 
henok_jskeet profile image
Henok Tesfaye

Finally, there are no more tasks to run in the global execution context! The event loop checks to see if there are any microtasks queued up: and there are! The async myFunc function is queued up after resolving the valued of one. myFunc gets popped back onto the call stack, and continues running where it previously left off.

If we assume function one will resolve eventually after getting http response. Will myFunc added to the micro queue when function one resolves, because if it's added immediately, the event loop will execute it when call stack is empty even if the promise don't resolve? If so there should be a different thread to handle(wait) till promise resolved which didn't cover in the animation.

Collapse
 
silverbullet069 profile image
VH • Edited

Now I understand "The await keyword suspends the async function, whereas the Promise body would've kept on being executed if we would've used then!"

If you call:

  • a function that return a Promise
  • an anonymous Promise that was defined inside that function.

It will run the callback inside that Promise and resolved that Promise instantly before calling then() since Promise constructor called its callback function.

Okay, now, inside an async function, when you encounters an await statement, you run the awaited command (either a function that return a Promise const res = await thisFuncReturnAPromise(), or that Promise directly const res = await promise), after that, you put that async function inside microstack, waiting to be resumed. That means the async function has been suspended.

P/s: in async function, we all know that async function will return a Promise, but we usually allude this because the return value of an async function is only valuable when that Promise call then() or being passed after await keyword in another async function.

Collapse
 
iosonoagenda profile image
Ettore Ongaro

Maybe if you share a piece of code can be more verbose man

Collapse
 
yoshcode profile image
Aljosha Novakovic

Wow who would have known that the event loop is really not that complicated at all, just requires a clear explanation, thank you!

I have one question, I was following everything until the last sentence! But maybe I'm just misunderstanding it:

"...The await keyword suspends the async function, whereas the Promise body would've kept on being executed if we would've used then!"

The await keyword suspends anything below it, within it's scope (the async function), until it is resolved. And if you saved that await call in a variable, you can make use of that return value. This code below the await keyword will be on the microtask queue until it is pushed to callstack and executed, like you very clearly explained. But doesn't the Promise "body" also suspend until it is resolved? The callback that you pass into the .then() will only get executed after our promise has been resolved... and similarly to how we can make use of the value we got from await() if we stored it in a variable, we can make use of the resolved value with .then(data => etc...) So don't technically both of them get suspended equally until they receive the resolved value (or rejected value)? Perhaps I'm just interpreting "Promise body" differently...

Thanks!

Collapse
 
cname87 profile image
cname87

Yes. It's a great article but it should consider the case of awaiting an API call or similar that takes a significant time to return. The simple example using Promise.resolve is not a typical use case. A visualisation of how such api calls would be really useful (although probably time consuming to produce).

Collapse
 
thumbone profile image
Bernd Wechner • Edited

Love it! Awesome piece. I have some residual questions. It's an old article so not sure if you're still around watching and noticing comments and questions. We shall see.

  1. The first relates to the stack. it would help to have another layer of function call to better illustrate the stack with contents, but that aside, I have often seen something like main() which alludes to what you've called the Global Execution Context at the bottom of the stack. For the simple reason that it's commonly written that the event loop regains control (and checks the microtask queue) when the stack is empty. I wonder if you have thoughts on that?

  2. the async/await visualisations are awesome beyond any I have seen yet! I wonder if you would continue with one more article, that covers .then() with this level of clarity.

  3. For all it's awesomeness I am still struggling to understand exactly how the two executor callbacks work, most especially with regard to timing, and the microtask queue. Here is my musing (current thinking) which I'm looking to confirm or correct: When the promise is instantiated it passes a two default callbacks to the executor, and runs the executor immediately (on the stack). A call to the first argument will have the Promise update it's state from "pending" to "fulfilled" and a call to the second will have it updated to "rejected". When the executor is finished (returns) it should have called one of these (or the promise will never resolve). After it has finished the instantiator returns and the promise is one of its three states. A call to .then() (optional) will register one or two callback handlers (for fulfilled and rejected). If the state of the promise is currently fulfilled or rejected it will immediately queues the appropriate callback onto the microtasks queue and return. If the state is pending, then the Promise itself will, as soon as its resolver or rejecter are called queue the appropriate callback then. I'm not sure if that's entirely accurate and would love to see it documented as well as you have if it is, and or what is right if it's not.

  4. When await is used, there is a situation I'm not clear on too. Await works a bit like yield I'm told, and it seems maintains the state of the running async function (on the heap one presumes) and as you write puts the async function on the microtask queue to continue at the same line (which the preserved state enables, though said state could be on the heap or on the microtask queue who knows?). In your example the function one resolves quickly, instantly, but if the function we are waiting on is still pending when the async function reaches the head of the microtask queue and runs once more, what then? I would guess it simply requeues itself. But it would be great to know and to have whatever happens there, so excellently illustrated as you have.

I would be so thrilled if you lent you expert hand at illustrating answers to these. Many many thnaks for the awesome work you have done to date!

Collapse
 
ahmadawais profile image
Ahmad Awais ⚡️

Great work, Lydia. I wish you'd use videos instead of Gifs next time, my Macbook Pro is acting out on loading so many gifs. LOL. Respect for all the work you put into this.

Collapse
 
lydiahallie profile image
Lydia Hallie

Yeah I know the pain, I can't change this currently as dev.to doesn't support video 😭

Collapse
 
trasherdk profile image
TrasherDK

Those animations works perfectly.

Had it been a video, I wouldn't have read through your excellent presentation.

Collapse
 
ahmadawais profile image
Ahmad Awais ⚡️

I'd recommend uploading to YouTube an embedding it here. I know it's less than ideal.

Collapse
 
pke profile image
Philipp Kursawe • Edited

Thanks for the writeup!

There is slight room for improvement though I think to just hand in the then handler functions, which in turn receive the correct input automatically that way:

getImage("./image")
  .then(compressImage)
  .then(applyFilter)
  .then(saveImage)
  .then(res => console.log("Success"))
  .catch(error => console.error(error))
Enter fullscreen mode Exit fullscreen mode

Some comments may only be visible to logged-in visitors. Sign in to view all comments.