1 2 3 4 5 6 7
Whereas, if you look at the README for node-fibers, you’ll see this pleasant-looking example:
1 2 3 4 5
That looks pretty sweet. It’s a synchronous version of
that doesn’t block the main thread. This seems like a nice combination
of the sequential style of synchronous code but with the
responsiveness of non-blocking I/O. Why wouldn’t we want something
like this in ECMAScript?
Coroutines are almost as pre-emptive as threads
Once you add coroutines, you never know when someone might call
yield. Any function you call has the right to pause and resume you
whenever they want, even after any number of spins of the event
loop. Now any time you find yourself modifying state, you start
worrying that calling a function might interrupt some code you
intended to be transactional. Take something as simple as swapping a
couple fields of an object:
1 2 3
What happens if
munge does a
yield and only resumes your code
after a few other events fire? Those events could interact with
and they’d see it in this intermediate state where both
obj.bar are the same value, because
obj.bar hasn’t yet been
We’ve seen this movie before. This is just like Java’s threads, where any time you’re working with state, you have to worry about who might try to touch your data before it reaches a stable point. To be fair, life is actually far worse in Java, where almost every single basic operation of the language can be pre-empted. But still, with coroutines, every function call becomes a potential pre-emption point.
Host frames make coroutines unportable
engine doesn’t use a stack (and they all do), coroutines would have to
be able to save a stack on the heap and restore it back on the stack
host language (usually C++)? Some engines implement functions like
Array.prototype.forEach in C++. How would they handle code like
1 2 3 4 5 6 7
Other languages with coroutines take different approaches. Lua allows implementations to throw an error if user code tries to suspend host activations. This would simply be unportable, since different engines would implement different standard libraries in C++.
The Scheme community tends to demand a lot from their continuations,
so they expect functions like
map to be
suspended. This could mean either forcing all the standard libraries
to be self-hosted, or using more complicated implementation strategies
than traditional stacks.
Simply put: browser vendors are not going to do this. Modern JS engines are extraordinary feats of engineering, and rearchitecting their entire stack mechanism is just not realistic. Then when you consider that these changes could hurt performance of ordinary function calls, well… end of discussion.
Shallow coroutines to the rescue
OK, back to the pyramid of doom. It really does kind of suck. I mean, you could name and lift out your functions, but then you break up the sequential flow even worse, and you get a combinatorial explosion of function arguments for all those upvars.
This is why I’m excited about
are a lot like coroutines, with one important difference: they only
suspend their own function activation. In ES6,
yield isn’t a
function that anyone can use, it’s a built-in operator that only a
generator function can use. With generators, calling a JS function is
as benign as it ever was. You never have to worry that a function call
yield and stop you from doing what you were trying to do.
But it’s still possible to build an API similar to node-fibers. This is the idea of task.js. The fibers example looks pretty similar in task.js:
1 2 3 4 5
The big difference is that the
sleep function doesn’t implicitly
yield; instead, it returns a
task then explicitly
yields the promise back to the task.js
scheduler. When the promise is fulfilled, the scheduler wakes the task
back up to continue. Hardly any wordier than node-fibers, but with the
benefit that you can always tell when and what you’re suspending.
Coroutines no, generators yes