Making promises in a synchronous manner

JavaScript Promises have been with us for a while. In fact they're with us more and more thanks to native browser implementations. In a nutshell, promises are a useful abstraction that makes handling asynchronous tasks less painful, while making code more readable, maintainable and less error-prone. Basically they allow us to replace code like this:

'use strict';

asyncTask1((err, result1) => {
  if (err) {
    handleError(err);
    return;
  }

  asyncTask2(result1, (err, result2) => {
    if (err) {
      handleError(err);
      return;
    }

    asyncTask3(result2, (err, result3) => {
      if (err) {
        handleError(err);
        return;
      }

      //...
    });
  });
});

with code like this:

'use strict';

asyncTask1()
    .then(asyncTask2)
    .then(asyncTask3)
    //...
    .catch(handleError);

Since we're mostly coding for browsers, where every line of synchronous (blocking) JavaScript basically results in an unresponsive UI, we're sort of obligated to take the non-blocking approach. While the intent behind it is quite obvious, asynchronous APIs (like promises) still lack the expressiveness of synchronous ones. Async by its definition means that the code will be executed at some unspecified time in the future, but not earlier than in the next tick of the event loop. What does this mean in practice? To put it as simply as possible: async JavaScript does not execute as it reads – top to bottom. Let's take a look at the following piece of code:

'use strict';

let asyncTask = () =>
  new Promise(resolve => {
    resolve();
  });

asyncTask().then(() => {
  console.log(1);
});

console.log(2);

Even though the promise in the example above get's resolved immediately, the code still logs '2' followed by '1'. Because the callback passed to .then() gets executed asynchronously, JavaScript engine has to first finish all its current jobs (which happens to be logging '2' to the console) before the event loop can pick another item from the callback queue and push it to the stack. For that reason, getting the resolve value of a promise synchronously (even a resolved one) is not possible. Wouldn't it be super-cool though, if we could (keeping all the goodness asynchronicity brings) write code like this:

'use strict';

try {
  let result1 = asyncTask1();
  let result2 = asyncTask2();

  console.log(result1, result2);
} catch (err) {
  handleError(err);
}

But, wait… can't we? Enter generator functions.

Among all the new exciting stuff we'll find in the ES6 spec draft, there's the concept of generator functions. These are a special kind of function in both syntax (asterisk notation) and semantics. Unlike regular functions, generator functions return something that's also new to ECMAScript: iterators. Iterators happen to be objects made specifically to be iterated on, e.g. with the all new for…of loop. They can be also iterated on manually by calling their 'next' method. Each such call produces an object containing two properties: 'value' (iterator's current value) and 'done' (a boolean indicating whether we reached the last value of the iterable). However, the best thing about generator functions is their ability to suspend their execution each time a keyword 'yield' is encountered. Let's have a glimpse of how it all works together:

'use strict';

function* generator() {
  console.log(0);

  yield 1;
  yield 2;
  yield 3;
}

let iterator = generator();
let current;

do {
  current = iterator.next();
  console.log(current.value);
} while (!current.done);

First we create an iterator simply by calling the generator function. When iterator's 'next' method is first called, the execution proceeds to the first 'yield' keyword ('0' is logged to the console). Whatever follows the 'yield' keyword directly is then assigned under the 'value' key in the object returned from current 'next' call. When 'next' is called again, the execution resumes until another occurrence of the 'yield' keyword and so on, until there are no more occurrences. Then, the iterator is considered complete and the value of 'done' in the returned object becomes 'true.' A generator function's return value (or 'undefined' if there's no explicit 'return' statement) is then accessible under the 'value' key.

Still, that's not enough for generator functions to act as control flow abstraction. What makes that possible is their ability to be passed values. These values are then injected where the last 'yield' keyword occurred.

'use strict';

function* generator() {
  let a = yield;

  return 2 * a;
}

let iterator = generator();

iterator.next();
console.log(iterator.next(2));

In the above example, when the 'yield' keyword is encountered, the execution is suspended until a value (optional) is passed back (as an argument for the 'next' call). When that happens, the value gets assigned to the 'a' variable within the generator function's body. And since the execution is actually postponed until this happens, we can really take our time here performing async operations e.g. waiting for the promise to resolve. When we're done, we just pass the value back.

Putting it all together:

'use strict';

let asyncTask = () =>
  new Promise(resolve => {
    let delay = Math.floor(Math.random() * 100);
  
    setTimeout(function () {
      resolve(delay);
    }, delay);
  });

let makeMeLookSync = fn => {
  let iterator = fn();
  let loop = result => {
    !result.done && result.value.then(res =>
      loop(iterator.next(res)));
  };

  loop(iterator.next());
};

makeMeLookSync(function* () {
  let result = yield asyncTask();

  console.log(result);
});

First we define the 'asyncTask' function. When called, it returns a promise which will be resolved with a random delay. The return value of that promise will be the delay itself. For the sake of simplicity, it always resolves. What makes the magic happen here though, is the 'makeMeLookSync' function. It takes a generator function as a parameter, then executes it and calls 'next' on the returned iterator until it's complete. In its simple version it assumes that the generator always yields a promise. It then waits for that promise to resolve and passes the resolve value back. The only difference between our final code and the desired one is the 'yield' keyword and the fact that it's wrapped in a generator function. That's a small trade off when compared to all the benefits it carries.

One big thing that's missing in this example is error handling. When it comes to asynchronous code, it's always been a bit tricky. Since the code in which an error can potentially be thrown (the callback) is executed in a different tick of the event loop, a simple try-catch won't work. But within the body of a generator function we don't need to care about that anymore. Since all async tasks can be performed 'outside', we can intercept errors with good old try-catch. What makes that possible is the ability of generators to be thrown errors. It's done by simply calling 'throw' where we'd normally call 'next' and passing the error object as a parameter.

This is how our previous example, updated with some basic error handling capabilities, could look like:

'use strict';

let asyncTask = () =>
  new Promise((resolve, reject) => {
    if (Math.random() > 0.5) {
      resolve(1);
    } else {
      reject(new Error('Something went wrong'));
    }
  });

let makeMeLookSync = fn => {
  let iterator = fn();
  let loop = result => {
    !result.done && result.value.then(
      res => loop(iterator.next(res)),
      err => loop(iterator.throw(err))
    );
  };

  loop(iterator.next());
};

makeMeLookSync(function* () {
  try {
    let result = yield asyncTask();
  
    console.log(result);
  } catch (err) {
    console.log(err.message);
  }
});

That's all folks! Happy coding!