Syntactic sugar: yet another async/await question for JavaScript interviews
Tags: javascript, async, await, promise
Published: 2021-05-04 11:08:55

Whenever I stumble upon *"[something] is a syntactic sugar"*, I appreciate when it is accompanied by a good technical explanation of *what* exactly that particular "sugar" is translated to behind the scene. Which isn't always the case.

For instance, try googling ["async await syntactic sugar"](https://www.google.com/search?q=async+await+syntactic+sugar). I don't think the statements like *"async is a syntactic sugar for promises"* are very helpful in grokking `async`/`await`. In my opinion, the concept of the [*finite-state machine*](https://en.wikipedia.org/wiki/Finite-state_machine) would be very important in this context, yet I couldn't spot the phrase *"state machine"* in the top Google-quoted results. 

So, here is one question I personally find interesting and relevant for the both sides of a JavaScript/TypeScript interview (as well as C#, F#, Python, Dart or any other programming language which has adopted the `async`/`await` syntax):

- *How would you go about implementing the following `async` function `<funcName>` as a simple state machine, without using the keywords `async`, `await` or `yield`?*

I think it's a one-shot-many-kills kind of question, potentially covering the knowledge of the basic topics like *promises*, *closures*, *exception handling*, *recursion*, in addition to `async`/`await` and the state machine concepts themselves.

For a practical JavaScript example, let's take the following simple asynchronous workflow function, `loopWithDelay`. It runs a loop doing something useful (`doWhat`), with a certain minimal interval between iterations, until the `stopWhen` callback signals the end of the loop: 

```JavaScript
async function loopWithDelay({ doWhat, stopWhen, minInterval }) {
  while (!stopWhen()) {
    const interval = startInterval(minInterval);
    await doWhat();
    const ms = await interval();
    console.log(`resumed after ${ms}ms...`);
  }
  console.log("finished.");
}
```

We might be calling `loopWithDelay` like below ([runkit](https://runkit.com/noseratio/loopwithdelay)). In JavaScript, anything can be awaited, so this works regardless of whether or not `doWhat` returns a promise:

```JavaScript
await loopWithDelay({
  doWhat: doSomethingForMs(150), 
  stopWhen: stopAfterMs(2000), 
  minInterval: 500
});

// a higher-order helper to simulate an asynchronous task
// (for doWhat)
function doSomethingForMs(ms) {
  let count = 0;
  return async () => {
    const elapsed = startTimeLapse();
    await delay(ms); // simulate an asynchronous task 
    console.log(`done something for the ${
      ++count} time, it took ${elapsed()}ms`);
  }
}

// a higher-order helper to tell when to stop
function stopAfterMs(ms) {
  const elapsed = startTimeLapse();
  return () => elapsed() >= ms; 
}

// a simple delay helper (in every single codebase :)
function delay(ms) { 
  return new Promise(r => setTimeout(r, ms)); }

// a higher-order helper to calculate a timelapse
function startTimeLapse() {
  const startTime = Date.now();
  return () => Date.now() - startTime;
} 

// a higher-order helper for a minimal interval delay
function startInterval(ms) {
  const sinceStarted = startTimeLapse();
  return () => {
    const sinceDelayed = startTimeLapse();
    return delay(Math.max(ms - sinceStarted(), 0))
      .then(sinceDelayed);
  };
} 
```

Of course, there are many ways to rewrite this `loopWithDelay` without using `async`/`await`. We don't have to stricly follow a typical state machine implementation as done by programming language compilers (which can be a bit intimidating, e.g., [look at what](https://www.typescriptlang.org/play?target=1&jsx=0&module=0#code/IYZwngdgxgBAZgV2gFwJYHsIwDbvQBwHVVkALAEQFNtgwAKAbxgBN1DThkAaGEZA9pQg8AtqggBJCMkoAnAG7BsMAL4BKGAwBQMGAHdSqbJRh0AhHwGkhdNRu27dUTHxjiZCpTAC8vZMFlkKQ9FbDoxSWk5ULUAbh1HYD1gEhY2DmRbeMcYZwhXERAfGCSU5DcozzC4hKcXdGMAOlwAczoAA1lKEAQRSmYSuA8YABIGQpVCxun2mt0VBLyQBspm9DaAIjhxVBBrZkaNmpUgA) TypeScript generates when it targets ES5. Interestingly, when targeting ES2015, TypeScript transpiles `async`/`await` [using generators](https://www.typescriptlang.org/play?target=2&jsx=0&module=0#code/IYZwngdgxgBAZgV2gFwJYHsIwDbvQBwHVVkALAEQFNtgwAKAbxgBN1DThkAaGEZA9pQg8AtqggBJCMkoAnAG7BsMAL4BKGAwBQMGAHdSqbJRh0AhHwGkhdNRu27dUTHxjiZCpTAC8vZMFlkKQ9FbDoxSWk5ULUAbh1HYD1gEhY2DmRbeMcYZwhXERAfGCSU5DcozzC4hKcXdGMAOlwAczoAA1lKEAQRSmYSuA8YABIGQpVCxun2mt0VBLyQBspm9DaAIjhxVBBrZkaNmpUgA) as an optimization). 

To implement `loopWithDelay` manually as a state machine, we need to break down the normal flow control statements (in our case, the `while` loop) into individual states. These states will be transitioning to one another at the points of `await`. Here's one take at that, `loopWithDelayNonAsync` ([runkit](https://runkit.com/noseratio/loopwithdelaynonasync)):

```JavaScript
function loopWithDelayNonAsync({ doWhat, stopWhen, minInterval }) {
  return new Promise((resolveWorkflow, rejectWorkflow) => {
    let interval;

    // a helper to transition to the next state, 
    // when a pending promise from 
    // the previous state is fulfilled
    const transition = ({ pending, next }) => {
      // we fail the whole workflow when 
      // the pending promise is rejected or
      // when next() throws 
      pending.then(next).catch(rejectWorkflow);
    }

    // start with step1
    step1();
    
    // step1 will transition to step2 after completing a doWhat task
    function step1() {
      if (!stopWhen()) {
        // start the interval timing here
        interval = startInterval(minInterval);
        // doWhat may or may not return a promise, 
        // thus we wrap its result with a promise
        const pending = Promise.resolve(doWhat());
        transition({ pending, next: step2 }); 
      }
      else {
        // finish the whole workflow 
        console.log("finished.");
        resolveWorkflow();
      }
    }

    // step2 will transition to step3 after completing a delay
    function step2() {
      transition({ pending: interval(), next: step3 }); 
    }

    // step3 will transition to step1 after showing the time lapse
    function step3(prevStepResults) {
      // prevStepResults is what the pending promise 
      // from step2 has been resolved to
      console.log(`resumed after ${prevStepResults}ms...`);
      step1();
    }
  });
}

await loopWithDelayNonAsync({
  doWhat: doSomethingForMs(150), 
  stopWhen: stopAfterMs(2000), 
  minInterval: 500
});
```

Equipped with `async`/`await`, we should never have to write code like `loopWithDelayNonAsync` in real life. It still might be a useful exercise though, especially for folks who first got into JavaScript [after it had received](https://v8.dev/blog/v8-release-55) the native support for `async` functions. 

Rather than taking `async`/`await` syntactic sugar for granted, I think it helps to understand how it works behind the scene as a state machine. It also amplifies how versatile, concise and readable the `async`/`await` syntax is.

For a deep dive into `async`/`await` under the hood in JavaScript, the V8 blog has an awesome article: ["Faster async functions and promises"](https://v8.dev/blog/fast-async).