An explanation of Promises and async functions

Author: zeel
Message:

The simple way to think of a Promise is like ordering something from an online store.

You buy a book on Amazon. They “Promise” to deliver it to you at some point in the future. You don’t need to sit around for two days waiting for it though, you can go about doing other things until it arrives. Once the packege comes, the Promise is “fulfilled” and you now have a book. It’s also possible for the package to get lost, so it could also “reject” instead.

Now imagine that you ordered a TTRPG book. You want to play a game of that TTRPG, but don’t yet know the rules. You want to prepare for it, but you don’t have the book. You have a task - prepping your session - that has to come after the book comes.

You, as a human, implicitly understand and won’t attempt to prep the session before the book comes, because that would be silly. But JS isn’t that smart. If your program gets a Promise, it needs told explicitly when there are tasks that need to come after the Promise is fulfilled.

Author: zeel

Message:

JS offers two ways of doing this: Promise.then() uses the callback style syntax. Much like an event handler, you pass then() a function, and JS will call that function only once the Promise is fulfilled - it won’t call prepGameSession() until the package with the book in it is delivered. Often, multiple Promises are chained together, with each .then() returning another Promise.

function beGM() {
  orderBook()  // The promise fullfills when the order is delivered
    .then((book) => prepGameSession(book))
    .then((session) => playTTRPG(session));
}

The other, somewhat more straightforward but often more mysterious method is async and await. The more important keyword here is await it means “stop here until the next thing is done.” When you use this method, it’s a bit more clear what you are doing, though it introduces a quirk: You can’t await if your function isn’t async, and the return value of an async function is always a Promise - this can cause some confusion, and a cascade of refactoring as many methods get converted to async in an attempt to make things work as expected.

async function beGM() {
  const book = await orderBook();
  const session = await prepGameSession(book);
  playTTRPG(session);
}

When creating a module, you often need to interact with the server. Just like ordering a book, when you interact with the server you might need to wait for a response. If you don’t, you might end up acting on data that doesn’t exist, or hasn’t changed, or doesn’t make sense. Or you might accidentally tell the server to update two things at the same time, which it doesn’t handle very well.
Another thing to watch out for, are loops.

JS has some nifty loops attached to the Array type, things like forEach and map and reduce. But these methods can cause some issues with Promises.

Imagine you want to start reading a new series of books. But you are unsure if you will like them, and you are on a budget. You want to order them, one at a time, and not order the next book until you finish reading the one that comes before.

If you do something like this:

for (let book of books) {
  let package = await order(book);
  let opinion = await read(package.book);
  if (!opinion.isGood()) break;
}

It works great, you will order a book, read it, and if you like it you will order the next. If you ever dislike one, you will stop ordering and exit the loop.

But what about forEach? :

books.forEach(async (book) => {
  let package = await order(book);
  let opinion = await read(package.book);
  if (!opinion.isGood()) /* Uh, you can't actually use break in one of these... */;
});

Looks the same right? But notice, the arrow function passed to forEach it has to be declared as async. Funny thing is, all async functions return Promises. And they return immediately. The forEach method of Array doesn’t know this, and it isn’t an async method - it won’t await each iteration of the loop! It will instead call the function once for each item no matter what! Suddenly it orders the entire series of books, all at once! And you get a really nasty credit card bill.
(Imagine you did this for Discworld, over 40 books at about $10 a pop! Though luckily it’s Pratchett, so no worries about disliking them.)