tim@kolberger.eu
Photo by Erik Witsoe on Unsplash

From async await and Promises

I guess here should be an introduction, but it is still pending.

About serial and parallel execution of asynchronous code.

Where we came from

The first goto solutions in JavaScript for asynchronous behavior was the callback pattern. It all started looking like this:

// 📞 create a callback function
const someCallbackFunction = (callback) =>
  callback('I was created by an callback function');

const someOtherCallbackFunction = (callback) =>
  callback('I was created by some other callback function');

someCallbackFunction(
  // 🏁 callback function will be executed when necessary
  (someValue) => {
    someOtherCallbackFunction((otherValue) => {
      // 🚫 this pattern leverages nesting: "callback hell"
      console.log({
        someValue, // -> 'I was created by an callback function'
        otherValue, // -> 'I was created by some other callback function'
      });
    });
  }
);

Promises to the rescue

The king is dead. Long live the king.

// create a plain promise with resolve shorthand
const createSomePromise = () => Promise.resolve('I was a promise');
// is same as `new Promise(resolve => resolve('I was a promise'))`

const createSomeOtherPromise = () =>
  Promise.resolve('I was some other promise');

// store the promise for later use
const mainPromise = createSomePromise()
  .then(console.log) // -> I was a promise
  .then(() => createSomeOtherPromise())
  .then(console.log) // -> I was some other promise
  .catch(console.error);

New syntax to the rescue?

The king is dead. Long live the king. A new syntax for asynchronous flow control was introduces with the keywords async und await.

// create an async function
const someAsyncFunction = async () => 'I was created by an async function';
// store the result - we are going to check what it is
const someAsyncFunctionResult = someAsyncFunction();

const createSomeOtherPromise = () =>
  Promise.resolve('I was some other promise');
const someOtherPromise = createSomeOtherPromise();

// we need an async function to `await` a value
async function main() {
  // one after another
  const someValue = await someAsyncFunctionResult;
  const otherValue = await someOtherPromise;

  // or in parallel
  /*
  const [someValue, otherValue] = await Promise.all([
    someAsyncFunctionResult,
    someOtherPromise,
  ]);
*/

  console.log({
    someAsyncFunctionResult, // -> Promise { 'I was created by an async function' }
    isPromise: someAsyncFunctionResult instanceof Promise, // -> true
    someValue, // -> 'I was created by an async function'
    otherValue, // -> 'I was some other promise'
  });
}

// we execute the async function and catch possible errors
main().catch(console.error);

Why not both?

Using Promises or async await is not mutual exclusive. We require the Promises prototype functions to choose the best tool for the job.

There are at least two different scenarios we want to master:

Scenario #1 - execution in parallel

There is no counterpart in the async await syntax to match the behaviour of Promise.all().

We want to do many tasks at once, we don’t care about when every result has arrived, but we want to continue only if all values arrived. Like waiting for your kids when walking home from the park - you don’t care which one arrives first, but you only leave if every single one of them are present.

function sleep(seconds, index) {
  return new Promise((resolve) =>
    setTimeout(
      () =>
        resolve({
          index,
          seconds,
        }),
      seconds * 1000
    )
  );
}

const sleepDurations = [0.5, 0.1, 0.5, 1];

function processInParallel() {
  return Promise.all(sleepDurations.map(sleep));
}

async function main() {
  console.time('main');
  const results = await processInParallel();
  console.timeEnd('main');

  console.log(results);
  /* -> logs all execution results at once in an array
    [
      { index: 0, seconds: 0.5 },
      { index: 1, seconds: 0.1 },
      { index: 2, seconds: 0.5 },
      { index: 3, seconds: 1 }
    ]
    main took ~1s
  */
}

main().catch(console.error);

There are more handy functions on the Promise prototype like Promise.allSettled and Promise.race.

Scenario #2 - execution in serial

Again we want to do many tasks, but not at once. One task after another. But we do not know how long each task will take. So we wait until the first one resolves, and proceed to the next. Like eating a 5 course menu. You don’t know how long each course will take, but you will start each course one after another.

function sleep(seconds, index) {
  return new Promise((resolve) =>
    setTimeout(
      () =>
        resolve({
          index,
          seconds,
        }),
      seconds * 1000
    )
  );
}

const sleepDurations = [0.5, 0.1, 0.5, 1];

async function processInSerial(onResult) {
  const promises = sleepDurations.map((duration) =>
    // create an async function which will be called by `processInSerial`
    async () => sleep(duration)
  );
  for (let fn of promises) {
    const value = await fn();
    onResult(value);
  }
}

async function main() {
  console.time('main');
  await processInSerial(console.log);
  console.timeEnd('main');

  /* -> logs one execution result after the previous
    { index: 0, seconds: 0.5 }
    -> then
    { index: 1, seconds: 0.1 }
    -> then
    { index: 2, seconds: 0.5 }
    -> then
    { index: 3, seconds: 1 }
    ----
    main took ~2.1s = 0.5s + 0.1s + 0.5s + 1s
  */
}

main().catch(console.error);
© 2021 Tim KolbergerDatenschutzerklärung