What is a state machine in terms of JavaScript promises and C# asyc-await?

I’m currently looking at async-await in C#, and noticed similarities to JavaScript promises. Looking into this I see that JavaScript is also going to support async-await statements, and that there are similarities between this and promises (look at this blog post for example).

On a whim, I wondered what JavaScript’s implementation of async-await was and found this question (Java Equivalent of C# async/await?).

The accepted answer suggests that async-await (and by extension, I guess, promises) are implementations of a ‘state machine’.

Question: What is meant by a ‘state machine’ in terms of promises, and are JavaScript promises comparable to C#’s async-await?

Answer

JavaScript promises are comparable to C# Task objects which have a ContinueWith function that behaves like .then in JavaScript.

By “state machines” it is means that they are typically implemented by a state and a switch statement. The states are places the function can be at when it runs synchronously. I think it’s better to see how such a transformation works in practice. For example let’s say that your runtime only understands regular functions. An async function looks something like:

async function foo(x) {
   let y = x + 5;
   let a = await somethingAsync(y);
   let b = await somethingAsync2(a);
   return b;
}

Now, let’s look at all the places the function can be when it executes a step synchronously:

async function foo(x) {
   // 1. first stage, initial
   let y = x + 5;
   let a = await somethingAsync(y);
   // 2. after first await
   let b = await somethingAsync2(a);
   // 3. after second await
   return b;
   // 4. done, with result `c`.
}

Now, since our runtime only understands synchronous functions – our compiler needs to do something to make that code into a synchronous function. We can make it a regular function and keep state perhaps?

let state = 1;
let waitedFor = null; // nothing waited for
let waitedForValue = null; // nothing to get from await yet.
function foo(x) {
   switch(state) {
      case 1: { 
        var y = x + 5;
        var a;
        waitedFor = somethingAsync(y); // set what we're waiting for
        return;
      }
      case 2: {
         var a = waitedForValue; 
         var b;
         waitedFor = somethingAsync(a);
         return;
      }
      case 3: {
        b = waitedFor;
        returnValue = b; // where do we put this?
        return;
      }
      default: throw new Error("Shouldn't get here");
   }
}

Now, it’s somewhat useful, but doesn’t do anything too interesting – we need to actually run this as a function. Let’s put the state in a wrapper and automatically run the promises when they’re resolved:

function foo(x) { // note, not async
  // we keep our state
  let state = 1, numStates = 3;
  let waitedFor = null; // nothing waited for
  let waitedForValue = null, returnValue = null; // nothing to get from await yet.
  // and our modified function
  function stateMachine() {
    switch(state) {
      case 1: { 
        var y = x + 5;
        var a;
        waitedFor = somethingAsync(y); // set what we're waiting for
        return;
      }
      case 2: {
         var a = waitedForValue; 
         var b;
         waitedFor = somethingAsync(a);
         return;
      }
      case 3: { 
        b = waitedFor;
        returnValue = b; // where do we put this?
        return;
      }
      default: throw new Error("Shouldn't get here");
   }
   // let's keep a promise for the return value;
   let resolve, p = new Promise(r => resolve = r); // keep a reference to the resolve
    // now let's kickStart it
   Promise.resolve().then(function pump(value) {
      stateMachine();
      state++; // the next state has progressed
      if(state === numStates) resolve(returnValue); // return the value
      return Promise.resolve(waitedFor).then(pump);
   }); 
   return p; // return the promise
}

Effectively, the Promise.resolve().then(... part calls the stateMachine and waits for the value that’s being awaited every time until it is at the final state at which point it resolves the (returned beforehand) promise.

This is effectively what Babel or TypeScript do with your code too. What the C# compiler does is very close – with the biggest difference is that it’s put in a class.

Note we are ignoring conditionals, exceptions and loops here – it makes things a little bit more complicated but not much harder (you just need to handle each case separately).

Leave a Reply

Your email address will not be published. Required fields are marked *