Are you sure you know Promises?

latobibor

András Tóth

Posted on June 29, 2022

Are you sure you know Promises?

I am reviewing other developers' code for a long time by now and saw lots of little mistakes and misunderstandings about how async code works in the language.

I think most people learning javascript eventually gets Promises, but there are lots of tiny edge-cases and nitty-gritty details. There is the high level understanding of the syntax and semantics of it, but since it's no trivial matter, it requires the "gut instinct" as well to be developed. You need to train your natural neural network with a long list of examples to develop this ability.

So here it is! An exhaustive list of many different cases (trivial and not so trivial) for people who learnt or are already familiar with Promises and are looking for nailing them.

How you should use the exercises

First, I will define a very simple function, that creates a new "simulated process", that outputs its ID and its start and finish. Afterwards it returns its id, that can be used for later processing.

Copy that into a browser console and execute it.

👀 Then look at the example.

🤔 Try to guess in what order will be the logs arriving to the output.

✅ When you made your guess copy-paste the example to the console and run it.

Found something that confused you? You have found a blind-spot! If you cannot work out what happened, you can ask me in the comments and I will explain! 😊

Note on guessing first

It is important to not just copy-paste these examples but to guess the results beforehand! The research is quite steady on Test-enhanced learning; so first you work your brain and then see if you were right.

function createProcess(id, timeout) {
  console.log(`🏃‍️ #${id}: Process started!`);

  return new Promise(resolve => {
    function runTinyFakeProcess() {
      resolve(id);
      console.log(`✅ #${id}: Process finished!`);
    }

    setTimeout(runTinyFakeProcess, timeout);
  });
}
Enter fullscreen mode Exit fullscreen mode

Examples

Example 1

Which process will finish first? Which will be the last?
Look for the ✅!

createProcess(1, 3000);
createProcess(2, 2000);
createProcess(3, 1000);
Enter fullscreen mode Exit fullscreen mode

Example 2

createProcess(1, 3000);
await createProcess(2, 2000);
createProcess(3, 1000);
Enter fullscreen mode Exit fullscreen mode

Example 3

await createProcess(1, 3000);
await createProcess(2, 2000);
await createProcess(3, 1000);
Enter fullscreen mode Exit fullscreen mode

Example 4

await createProcess(1, 3000).then(() => {
  createProcess(2, 2000);
  createProcess(3, 1000);
});
Enter fullscreen mode Exit fullscreen mode

Example 5

await createProcess(1, 3000)
  .then(() => createProcess(2, 2000))
  .then(() => createProcess(3, 1000));
});
Enter fullscreen mode Exit fullscreen mode

Example 6

await createProcess(1, 3000)
  .then(() => {
    createProcess(2, 2000);
  })
  .then(() => createProcess(3, 1000));
Enter fullscreen mode Exit fullscreen mode

Confused about the result and can't see the difference? Check for the {} around the second call!

Example 7

await Promise.all([
  createProcess(1, 3000), 
  createProcess(2, 2000), 
  createProcess(3, 1000)
]);
Enter fullscreen mode Exit fullscreen mode

Example 8

await Promise.all([
  createProcess(1, 3000)
    .then(() => createProcess(2, 2000)),
  createProcess(3, 1000)
]);
Enter fullscreen mode Exit fullscreen mode

Example 9

await Promise.all([
  await createProcess(1, 3000), 
  await createProcess(2, 2000), 
  await createProcess(3, 1000)
]);
Enter fullscreen mode Exit fullscreen mode

Example 9b

Got confused about example #9? Let's break the previous one down:

// But how??? Think about initialization!
const processes1 = [
  await createProcess(1, 3000), 
  await createProcess(2, 2000), 
  await createProcess(3, 1000)
];

console.log('Are the processes already done?');

// At this point all 3 processes will already be awaited
// and this call will immediately return the already 
// processed results.
await Promise.all(processes1);
Enter fullscreen mode Exit fullscreen mode

Example 10

Let's do something with the returned values!

const processes2 = [
  createProcess(1, 3000), 
  createProcess(2, 2000), 
  createProcess(3, 1000)
];

await Promise.all(processes2.map((item) => item * 2));
Enter fullscreen mode Exit fullscreen mode

Example 11

How did we get NaNs? 🤔 And how do get the proper answer?

Look!

const processes3 = [
  createProcess(1, 3000), 
  createProcess(2, 2000), 
  createProcess(3, 1000)
];

// first we wait for the processes to finish
(await Promise.all(processes3))
// then we just toy with the results
  .map(result => result * 2));
Enter fullscreen mode Exit fullscreen mode

Example 11b

Could we have done just this?

const processes4 = [
  createProcess(1, 3000),
  createProcess(2, 2000),
  createProcess(3, 1000)
];

await Promise.all(processes4.map(async (promise) => {
  const result = await promise;
  return result * 2;
}));
Enter fullscreen mode Exit fullscreen mode

Actually, yes, we could! But check for the sublety!
In #11, we waited for all promises to first run to completion and then we processed the result.

In #11b we are immediately acting upon the promises: we await them, process their result. Then Promise.all will wait for the new promises defined by the async .map function which in turn would yield the processed results.

Example 11c

Does creating processes and awaiting them in a .map() would make them run in series instead of in parallel? 🤔

const values = [
  { id: 1, time: 3000 }, 
  { id: 2, time: 2000 }, 
  { id: 3, time: 1000 }
];

// try it!
await Promise.all(
  values.map(async ([id, time]) => 
    await createProcess(id, time)
  )
);
Enter fullscreen mode Exit fullscreen mode

No, don't worry! During runtime when await is encountered, the "processing" of the code block will be suspended and another block can be processed. In other words, after createProcess started processing at the moment of await, we switch to another iteration of .map, which will in turn start another process.

Example 12

Let's check on Promise.race!

await Promise.race([
  createProcess(1, 2000),
  createProcess(2, 1000)
]);
Enter fullscreen mode Exit fullscreen mode

Who would "win"?

Note

Pay attention to the logs as well!
See anything strange?

Even though #2 was the first and the result of #1 is ignored, #1 also ran to completion! In case process #1 must stop when process #2 "won" the race, then you need
something like a cancellable promise! (There are other ways... You can do your own research by searching for "Why can't you cancel promises?").

Example 13

await Promise.race([
  createProcess(1, 2000)
    .then(() => createProcess(3, 3000)), 
  createProcess(2, 1000)
]);
Enter fullscreen mode Exit fullscreen mode

See my previous point? The entire chain ran (defined in the first item in the array), even though process #2 already "won" the race.

Example 14

What will be the last message? For this exercise I am going to pass in some arrays for IDs (I know it's not clean code), because createProcess merely returns the first parameters.

Therefore this time we can use the awaited return values to start and wait for even more processes.

So what this really should do is waiting for some array data and then run processes for each item in each arrays.

And after everything ran we would like to have have a success message. But did we succeed? 🧐

const processes5 = [
  createProcess([1, 2, 3], 1000),
  createProcess([4, 5, 6], 2000),
  createProcess([7, 8, 9], 3000)
];

function runPromiseAll1() {
  return Promise.all(
    processes5.map(async process => {
      const ids = await process;

      await Promise.all(
        ids.map((id, index) => {
          createProcess(`${id}->${index}`, Math.random() * 5000)
        })
      );
    })
  );
}

await runPromiseAll1();
console.log('👆 Must be the last message! We waited for everything!');

Enter fullscreen mode Exit fullscreen mode

Is it though?

14b - making order

Let's see if we can make that last console.log fired at the right time:

function runPromiseAll2() {
  return Promise.all(
    processes5.map(async process => {
      const ids = await process;

      return await Promise.all(
        ids.map(async (id, index) => {
          return await createProcess(`${id}->${index}`, Math.random() * 5000);
        })
      );
    })
  );
}

await runPromiseAll2();
console.log('👆 This _really_ should be the last message after we waited for everything!');
Enter fullscreen mode Exit fullscreen mode

14c: the flatMap version

And now let's see a "smart" version:

function runPromiseAll3() {
  return Promise.all(
    processes5
      .flatMap(async process => await process)
      .map((id, index) => createProcess(`${id}->${index}`, Math.random() * 5000))
  );
}

await runPromiseAll3();
console.log('👆 Finished!');
Enter fullscreen mode Exit fullscreen mode

Wait... What? The problem is that async code always wraps the result in a Promise, so even you return an array it will be always a Promise.

14d: the "Let's unpack the array first" version

Let me confess that I needed to try out a couple of things before I could fix this. Async programming combined with arrays in JS is not an easy task.

async function runPromiseAll4() {
  // we unpack the array from the Promise
  const ids = await Promise.all(processes5);

  // and now we can use whatever array 
  const newPromises = ids
      .flatMap(id => id)
      .map(item => { console.log(item); return item; })
      .map((id, index) => createProcess(`${id}->${index}`, Math.random() * 5000));

  return Promise.all(newPromises);
}

await runPromiseAll4();
console.log('😅 Finished!');
Enter fullscreen mode Exit fullscreen mode

Summary

If you find async programming hard, you are right! I think most of us gets the simple notion of some code blocks are running in parallel, however using a simple document to express it is a harder task. There are no visual aids on which blocks would run to completion, which can run in parallel. You have to really-really pay attention to the fine details of the code.

And I think it is a language design problem as well. Until we find a better to express it, we should hone the sharpness of our wits and eyes!

💖 💪 🙅 🚩
latobibor
András Tóth

Posted on June 29, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related