4

To my understanding, the point behind await is to 'await' the acting upon the resolved value of a promise until it is encountered as a microtask, as Jake Archibald explains here.

This video by LLJS shows that async-await is essentially syntactical sugar for a generator runner/interpreter function that yields where it awaits and passes the settled value of the promise to the .next() method. This means that the runner's execution of .next() is enqueued as a microtask when an await occurs.

Effectively speaking, all the code under that await will only be executed at the next microtask checkpoint. This can be an issue if code that doesn't require the awaited value of the promise lies underneath it, which is exactly the issue with Async IIFEs.

async function ping() {
  for (let i = 0; i < 5; i++) {
    let result = await Promise.resolve("ping");
    console.log(result);
  }

  console.log("Why am I even here?");
}
    
async function pong() {
  for (let i = 0; i < 5; i++) {
    let result = await Promise.resolve("pong");
    console.log(result);
  }

  console.log("I have nothing to do with any of this");
}
    
console.log("Let the games begin!");
ping();
pong();
console.log("Placeholder for code that is not related to ping pong");

In this example, the outside logs are logged first as part of the task of running the script, then the values of the resolved promises in the order that they were queued in the microtask queue. Within this entire process, the logs left underneath the for loops have nothing to do with the loops and are needlessly paused until the last microtask in their respective function bodies is out of the queue.

This is exactly what happens when we use async functions as IIFEs though. If you have code under the await that is meant to execute synchronously, it would have to needlessly wait until all the awaits above it have been checked out of the microtask queue.

I can see this being a problem if someone blindly wraps their entire express routes in async functions, where they would needlessly await the resolving of certain promises like database operations, the sending of emails, reading of files, etc..., So why do people still do this?

app.post('/forgotPwd', async (req, res) => {
  const {email, username} = req.body;

  if (!email) {
    res.status(400).json({error: "No username entered"});
    return;
  }

  if (!username) {
    res.status(400).json({error: "No email entered"});
    return;
  }

  const db = client.db();
  
  const user = await db.collection("Users").findOne({username: username, "userInfo.email": email});

  if (!user) {
    res.status(400).json({error: "Account not found"});
    return;
  }

  const authToken = await getAuthToken({id: user._id.toHexString()}, "15m");

  // Would probably send a more verbose email
  await sgMail.send({
    from: process.env.EMAIL,
    to: email,
    subject: 'Forgot Password',
    text: `Use this url to reset your password: http://localhost:5000/confirmation/passConf/${authToken}`,
  });

  res.json({error: ""});
});
Clever7-
  • 75
  • 1
  • 9
  • 2
    Minor terminology note: You mean *settled* (rejected or fulfilled) or *fulfilled*, not *resolved*. A promise can be resolved but still pending. – T.J. Crowder Dec 28 '20 at 12:40
  • Oh yeah that's true! I'll change that. – Clever7- Dec 28 '20 at 12:42
  • 2
    I wouldn't call that code *useless*, I'd just call it *wrong* if the author meant that part to execute immediately. – Bergi Dec 28 '20 at 12:44
  • 2
    "*if someone blindly wraps their entire express routes in async functions*" - can you show a concrete example please? I'm not sure what you are referring to. – Bergi Dec 28 '20 at 12:46
  • 1
    Thanks for adding an example, but which part of that code do you think is unrelated to the asynchronous calls and should be done immediately? – Bergi Dec 28 '20 at 13:02
  • Even though I'd be fine with awaiting the initial db lookups before the response is sent back, I would not be fine with awaiting the insertion of data into the database before sending a response back. – Clever7- Dec 28 '20 at 13:04
  • 2
    @Clever7- Of course you need to wait for that! It might reject, in which case an error response needs to be sent, so you cannot send the response before the database transaction finished. (Admittedly, proper error handling is *missing* in the code you showed, but that's not an excuse). – Bergi Dec 28 '20 at 13:07
  • Ah I see, I didn't consider that being an issue, but I get it now. I'll change it to a more practical example. – Clever7- Dec 28 '20 at 13:14
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/226518/discussion-between-clever7-and-bergi). – Clever7- Dec 28 '20 at 13:20

2 Answers2

3

If you want something in an async function to run synchronously, make sure it's prior to the first await in the function.

So why do people still do this?

That's probably an off-topic question for SO since it largely calls for opinion-based answers, but it's likely going to be either A) Because they don't want that code to run until the code above it has finished, or B) Because they don't understand async functions.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
3

The point of using async/await is to make asynchronous code look synchronous because it's easier to read. In fact, it is a syntactic sugar that hides a callback hell underneath. You don't need to deal with callbacks in order to handle async operations.

In your case, if you think that the code after the for loop has nothing to do with the awaited operation, you shouldn't have place it after await. Or you should refactor the code so that it does not use await (callbacks).

As for the question of why people do this. Well, can you tell why people use .map() as a replacement of .forEach()? Or can you tell why they don't handle exceptions? They probably don't fully understand it or (as T.J. Crowder mentioned) they do want the code to run after the awaited operation. Simple as that.

Sebastian Kaczmarek
  • 8,120
  • 4
  • 20
  • 38