loading...
Cover image for โญ๏ธ๐ŸŽ€  JavaScript Visualized: Promises & Async/Await

โญ๏ธ๐ŸŽ€ JavaScript Visualized: Promises & Async/Await

lydiahallie profile image Lydia Hallie Updated on ใƒป14 min read

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

Posted on by:

Discussion

pic
Editor guide
 

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...

 

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...

 

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

 

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.

 

Those are dope.

 

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. ๐Ÿง

 

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

 

Thank you so much!

 

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.

 

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?

 

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

 

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

 

One of the best, if not the best, explanation of promises. The explanation I wish I had and glad I got to read today. Excellent visuals too!

 

Ah thank you so much! ๐Ÿ˜ƒ

 

Hi Lydia
This an amazing article, also others that demonstrating by animation.

I want to ask you permission to translate your articles to Chinese to help more persons, also should have the original post link of this course.

 

I already read all your articles !! wonderfull and genious !! thank you for this huge effort and your articles are very helpful ! just one thing , in the callback hell example above in line 7, shouldn't you say "saveImage ( filteredImage , (res,err) => { "instead of compressedImage . thank you !

 

As soon as setTimeout api is encounter, its gets pushed out from execution callstack to webapi. Now when Promise encounter, its cb function gets pushed out to microtask callback queue. In terms of setTimeout I can understand that its WEB-API pushing cb function after timeout, but in case of promise (suppose it takes 2 min to get response from API) then how that point is trigger to push promise cb to microtask? (if it is part of JavaScript engine only why not execution is stucking there?)

 

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!

 

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).

 

I am not a JavaScript person. Btw, I am very curious to know how did you make these visualizations? Are there any tools you used for this?

 

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.

 

Yeah I know the pain, I can't change this currently as dev.to doesn't support video ๐Ÿ˜ญ

 

Those animations works perfectly.

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

 

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

 

Hi, I think I'm at the beginning but I think there is some mishap in one of the images res.cloudinary.com/practicaldev/im...
Shouldn't Promise constructor arguments names be the same as functions in try and catch blocks?
I hope it hasn't been pointed out before, but I have no time, gotta get back to reading of this gem ;)

 

I am very impressed your excellent blog. your visualization effort to understand the other interested in the javascript field engineer is very helpful , so Could you please allow me to translate it into Korean?

 

Thanks a lot for this article! After working with promises and Async code in JavaScript quite a lot recently, I had to take a few steps back and get on top of some of the basics. I stumbled across your article that I really love. Async for dummies! I certainly feel less dumb now! Thanks ๐Ÿ˜Š

 

Great read, love the visuals. Thanks a lot.

 

Great article and excelent GIFs

Saving to share with all collagues

Btw: there's a typo in res.cloudinary.com/practicaldev/im...

resolve is not found

 

I cannot even imagine how long it took to make this post! Absolutely stellar!

 

This is soooo cool! May I know what tool you using to make the visualize stuff?

 

To say that, I am impressed is a huge understatement. Only one word can describe this magnificent article and that is AWESOME. Keep up the great work. Super Lydia!

 

Super cool explanation and thanks for adding that GitHub repo at the end. Stay safez...!

 

Hi Lydia ! Great job !!
:D
Also, I know it's a very small typo, but you seem to have one opening bracket { too many in your image: res.cloudinary.com/practicaldev/im...
and since we're talking about code, I guess it'd break ^^

Thanks for opening the path for me to using async/await !

 

Noice! Always great work Lydia.

Noice!

 

Awesome work, Lydia. What an effort! And very valuable stuff too. Great thanks for sharing.

 

So micro task callback doesn't get pushed to web api's ?

 

Really its Great. I think I understand a lot more now :)
Thanks alot ๐Ÿ˜Š

 

thank you for this writing this post!
This visualization is great help to understand JS for me !!

 

This is fantastic, love the whole series!