DEV Community 👩‍💻👨‍💻


Posted on

await Promise !== coroutine

Yeah this is a sister post of Inversion of Inversion of Control. In that post I've illustrated the idea of coroutine (which invert the inverted control back) implemented by async/await. However, here I'd like to investigate it in depth and conclude that async/await syntax is not strictly coroutine.

Let's see an example.

The code:

const nextFrame = () => 
new Promise(resolve => requestAnimationFrame(resolve));

(async function animate() {
  while (true) {
    // ... maybe we should add an exit/break condition
    // lovely in-control-render-loop
    await nextFrame();
Enter fullscreen mode Exit fullscreen mode

But it has a problem: Your code is not executed in rAF callback synchronously but a micro-task callback. So intuitively you get zero benefit from using rAF.

Ironically you might never notice this as some implementations do cover the case. See Timing of microtask triggered from requestAnimationFrame

It's due to the spec of Promise: always trigger a micro-task. But in a real coroutine, the control is expected to be resumed at a specific point, synchronously. The rAF is such an example, and some libraries/frameworks would use black magic side-effect-ish global variable to store context informations in a synchronous procedure. (And luckily JavaScript is single-threaded, otherwise...). Anyway we need control back immediately, not delegated by a micro-task.

Someone may ask: why a Promise must be asynchronous? Can't we have a synchronous Promise? (off-topic: the executor function in Promise constructor is executed synchronously.) The answer is: it could be but it shouldn't be. Having an asynchronous model simplifies the design, as Promise represents the eventual result of an asynchronous operation. For a Promise we only concern the value (and/or reason for no value). So a Promise just tell you "I'll eventually give you a value but not sure about when it's available (and doesn't necessarily to be in a micro-task.)". Even a fulfilled/rejected Promise notifies the value asynchronously, to make the design consistent. So you know the callback in .then is always deferred. Otherwise,

// not runnable code, for illustration purpose
  // assume an error is thrown in callback
  throw 'Oops!!';
  // or access a closure variable
  doSomething(y); // 'y' is undefined if sync
// ... original flow of control
let y;
// ...
Enter fullscreen mode Exit fullscreen mode

a sync and async callback give different behavior.

So let's go back to coroutine. Can we have a proper coroutine in JavaScript? Of course, by Generator. You can implement your own scheduler, and decide when to return the control back. (But it doesn't seem to be easy as it is described 😅. I planned to list some implementations here but none of them is Promise-free). I'll continue on this topic.

Top comments (0)

In defense of the modern web

I expect I'll annoy everyone with this post: the anti-JavaScript crusaders, justly aghast at how much of the stuff we slather onto modern websites; the people arguing the web is a broken platform for interactive applications anyway and we should start over;

React users; the old guard with their artisanal JS and hand authored HTML; and Tom MacWright, someone I've admired from afar since I first became aware of his work on Mapbox many years ago. But I guess that's the price of having opinions.