DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

KyleJB
KyleJB

Posted on

A key difference between .then() and async-await in JavaScript

Picture of Asynchronous processes

Asynchronous code can be frustrating when its behaviors are not fully understood. In JavaScript, .then() and await are the most commonly used functions for handling asynchronous nature of a Promise. I'd like to take a stab at demystifying some of the quirks that make JavaScript feel "weird" in order to help us take full advantage of asynchrony.


When learning all about fetch() and the Promise fetch() returns, I was introduced to JavaScript's .then()function as a means of handling the asynchronous nature of a Promise. So, what's the deal with async and await? Is it just syntactic sugar to avoid .then()'s callback hell? Searching "async vs .then" in google produces front page results such as this stackoverflow post. But, as we all come to know on our coding journey, the devil is in the details...

In order to examine the behaviors of async-await and then, I built a small program. First, we need to create a database (I used json-server) to make our fetch requests.

Sample Database to Fetch from on localhost:3000

Then, we need to write two functions that are practically identical save for one detail - one has to use then and the other has to use async-await. Take note of the carefully placed console.logs , as we will be tracking their appearances shortly.

Fetch with .then()

Fetch with async-await

Towards the bottom of the file, I added two additional console.logs wrapped around my invocation of the aforementioned function, I run both separately like so:

Code snippet of how I call each function separately


Before you read further, what order will the console.logs come out in? Do take into account the console.logs within the function definition themselves too...

The results!

Output of console.logs after running function with .then()

Notice how then runs through the entire function and then continues the execution after the function invocation before returning back to the function to perform the series of then operations after the Promise was resolved?

Output of console.logs after running function with async-await

Contrasting this with Async/Await, the function halts execution within the function scope continues to execute other tasks that follow its invocation before stepping back into the promise upon its resolution; this behavior will continue until all Promises are resolved.

Pan Wangperawong, quoted below, summarizes the differences succinctly in his blog post, which I encourage you to check out if you'd like more context.

async-await

Useful to use if your code works withΒ PromisesΒ and needs to execute sequentially. Due to blocking, you might lose some ability to process code in parallel. I've primarily usedΒ async-awaitΒ when making API requests. β€”Pan Wangperawong

then

If you develop your frontend with React.js, a typical use case might be to display a loading screen until a fetch request returns and then using a setState to update the UI. β€”Pan Wangperawong

Top comments (6)

Collapse
sainig profile image
Gaurav Saini

But, as we all come to know on our coding journey, the devil is in the details...

true dat

It is a very dangerous thing to do something without knowing the consequences.

DISCLAMIER:

  • I've got nothing against async/await, I like it when used properly. It definitely looks way more cleaner than .then.
  • No offence intended to anyone. I'm only sharing my experience.

I'd like to share some of my experience. I always use .then in my code. I find it easy to work with and more importantly easy to navigate through.

I've seen so much careless use of async/await, IMO it's because it looks like synchronous code when its not. And I'm not talking about junior devs only. I've seen loops on 100s of array items with async/await in the loop body, when it could have been done with a simple Promise.all. Sure, that is not always doable, and Promise.all can be used with async/await, but I've rarely found that to be the case. An even better and highly underused construct is async generators.
Below is a very simplified example, but inspired from real projects

function simpleAsync() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ message: 'ok' });
    }, 500);
  });
}

function usingThen() {
  const promises = [];

  for (let i = 0; i < 100; i++) {
    promises.push(simpleAsync());
  }

  Promise.all(promises)
    .then((res) => {

      for (const r of res) {
        console.log(r);
      }
    });
}

async function usingAsync() {

  for (let i = 0; i < 100; i++) {
    const r = await simpleAsync();
    console.log(r);
  }
}

// usingThen();
usingAsync();
Enter fullscreen mode Exit fullscreen mode

Another major point is error handling. Since, async/await "looks" easy, it is even easier to mess it up. you misplace one try catch block and before you know it, your app will be crawling with silent failures, which IMO is the the biggest pain in the ***
It gets worse when people mix the two, I've seen such examples in real codebases I've worked on, and I can never forget it

function simpleAsync() {
  return new Promise(async function(resolve, reject) {
    const result = await someAsyncCall();
    return result;
    // WHYYYYYYYY U DO DIS
  });
}
Enter fullscreen mode Exit fullscreen mode

And, lastly if you're a perfectionist (I like to think I am)

I don't know when this started happening, probably sometime in early 2019, but promises are now placed in the microtask queue in the event loop, and the thing with this queue is that at the end of each tick it has to be emptied (all the items need to be processed), but the catch here is if you put more microtasks (either using queueMicrotask() or promises) during the same tick before the queue is emptied, they also have to be processed in the same tick so technically it has the potential to crash the app if not used with caution.
Why is this relevant you ask. When you use promises, the same promise object is tossed around when you return from method calls, etc.. But async await automatically wraps every value it is used with, with a new promise, effectively creating a myriad of promises. Combine that with the await in a long loop and you have the perfect recipe for a blackout.
See the Node trace flags to go into more details.

I highly recommend to check out James Snell's talk on broken promises.

Collapse
kylejb profile image
KyleJB Author

Guaurav Saini,

Thanks for your feedback! I really appreciate your perspective – and the video you recommended, which was amazing. I have a few follow-up questions for you, if you have the time.

I am not familiar with async generators. I'm digging into some documentation now. On first glance, async generators feel similar to how async-await behaved in my aforementioned example: the execution within the async function is paused until the Promise is resolved. Does the difference only lie with the typeof return value: Promise object vs Generator object?

If so, why is it preferred to make async-await behave like .then()? I figured async-await's rise in popularity is due to differences in behavior.

But async await automatically wraps every value it is used with, with a new promise, effectively creating a myriad of promises.

It was my understanding that async ensures that a function returns a promise by creating a new Promise only if a "thing" within the function is not a Promise. Am I missing something here when you caution about the "recipe for a blackout"?

Collapse
naimadozodrac profile image
NaimadOzodrac


It was my understanding that async ensures that a function returns a promise by creating a new Promise only if a "thing" within the function is not a Promise.

I think this is not exactly accurated. We are getting a promise, which resolves with the same value as the promise from the return statement but ...it is not true that the exact same object is returned, not even when there is already a promise, it is a new one.

const p = Promise.resolve('foo');

async function foo() {
return p;
}
p === foo(); // false

BTW the article is super nice, thank you

Thread Thread
sainig profile image
Gaurav Saini • Edited on

Oh, didn’t know it worked that way. Thanks for your input @naimadozodrac

Again as we find out, the devil lies in the details

Collapse
sainig profile image
Gaurav Saini

RE: async generators
This is somewhat an advanced topic that I have not used in a lot of projects, but it is very useful when handling multiple data emissions from an asynchronous operation/source (eg: continuous user interaction), kind of like streams. A good syntax to handle this is also the for await, it is better than awaiting inside of a loop, but unfortunately it doesn't fit quite well in every use case, hence the low popularity

for await (let val of myIterator) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

I can't remember some good code example, will have to look for some good examples/use-cases. I did use it in a small game I created using Pixi for the game loop or ticker or whatever you call that thing, but later replaced it with RxJS.

It was my understanding that async ensures that a function returns a promise by creating a new Promise only if a "thing" within the function is not a Promise.

You are correct, I forgot to include that detail.
But, like I said, it must be used with caution and one should not needlessly use it in front of everything.

RE: "recipe for a blackout"
I might have been a bit too dramatic there, but let me explain. See my note above for microtasks. it is difficult, but if someone managed to queue new microtasks (creating new Promises is one way of queuing a microtask) faster than the event loop can process them, then the loop gets stuck at the microtask queue and it will be stuck infinitely processing the microtasks.

Two important points here are:

  • I think it is impossible to completely halt the event loop at the microtask queue step, but the worst case scenario would be to have janky/laggy user experience even for very trivial tasks like reponding to click events, keyboard events, rendering DOM elements, etc.
  • async/await is not at fault here at all, this can be done with Promises and .then too, but it seems like it would go unnoticed easily with async/await as they can be used within loop bodies while .then can't be, atleast not the way you would expect it to behave (another reason I don't use await in loops)

I don't discourage the use of one asynchronous construct over the other, just that we should use things with caution, so that they don't backfire and atleast understand the basic working of anything before we use it

Collapse
ramesh profile image
Ramesh Elaiyavalli

@kylejb @sainig Good post and solid dialog. Understanding & keeping promises are very important. πŸ‘

πŸ€” Did you know?

Β 
πŸ“š You can adjust your experience level in Settings to see more relevant content.