…could be written as:
Interesting stuff, right? Centralized exception handling and a easy-to-understand flow control. Note: If you just have to know how “sync” is implemented, scroll to the “Blocking Ajax” example below.
Uhhh, ECMAScript 6?
ES6 Generators: Quick n’ Drrty
In order to understand what’s going on in the example above, we need to talk about what an ES6 Generator is and what it provides you.
A Modest Iterator
Whew. Now that we’ve gotten that out of the way, let’s see some code. We’ll build a simple iterator to demonstrate the suspend / resume semantics:
Here’s what’s happening:
- The caller, function “run,” first initializes the fibonacci generator (denoted by the “function*” syntax). Unlike a normal function, this does not cause the code in its body to be run – it simply returns a new generator object.
- When “run” calls the generator’s “next” method (a synchronous operation), the code is the generator’s body run… up until the “yield” keyword.
- Evaluating the “yield” operator suspends the generator and yields the generator’s result back to the caller. Operations following the yield have not yet been evaluated. The value (the operand, “a” of “yield”) will be accessible to the caller through the “value” property of the generator result.
- When the caller is ready to resume the generator, the “next” method is called and processing the code in the generator’s body continues immediately after where the prior “yield” left-off.
You may be wondering if the generator function will ever return. The answer is “no,” it will loop loop as many times as someone calls the “next” method.
Following the Flow: A Digression
As mentioned in the prior example, code in the generator function’s body encountered after the yield operation won’t be run until the generator is resumed. The generator can also be passed an argument, which will be substituted into the generator’s function body where yield left off:
The first time the generator’s body is run, “a” is yielded back to the caller (and made available through the “value” property of the returned object). The caller then resumes the generator, passing 10. Using substitution to visualize what’s happening:
The generator then hits the second “yield” statement and is suspended. The value “b” is available on the returned object. Finally, the caller resumes the generator, passing 2. With substitution:
The “pow” method is then called and the return value stored in the “result” variable which is then returned to the caller.
Fake Synchronicity: Blocking Ajax
Before we jump into the next example, note the “sync” function. This function calls the generator function with a resume function, and then calls “next” on it to get things started. Whenever the generator function needs an async call, it supplies resume as the callback and yields. When the async call executes resume, it calls “next” (with a value) on the generator, allowing it to continue execution with the result of the async call.
Okay, back to the codez:
Can you guess what you’re going to see in the console? If you said “foo,” “bar”, and “whatever was in blix.txt,” felicidades, compa. You’re right. By putting the code that we want to run in series inside a suspendable generator function, we can make it behave in a synchronous, top-to-bottom manner. We aren’t blocking the event loop thread; we suspend the generator and resume the non-generator code at the point at which we called “next.” The generator has been suspended but has not been garbage collected. The callback, called at some point in the future on a different tick of the event loop, resumes our generator, passing a value.
Centralized Exception Handling
Centralizing exception handling across various asynchronous callback functions is a pain. Take, for example, the following:
The catch block will never be hit (unless for some reason the synchronous calls to “firstAsync” or “secondAsync” or “thirdAsync” cause an error to be thrown) due to the execution of the callback being a part of a completely different call stack, on a separate tick of the event loop. Exception handling must be done in the callback body itself. One could write higher-order functions to eliminate some of the error-throwing duplication and remove some of the nesting with a library like async, but if we follow the Node.js error as “first argument” convention, we can write a generalized handler that will propagate all errors back to the generator:
Just because your generator code runs from top-to-bottom doesn’t mean you can’t handle multiple asynchronous operations concurrently. Libraries like genny and gen-run and co provide APIs do this, and basically reduce to yielding some enumeration of asynchronous operations to be completed before the generator is to be resumed. We can add basic support for concurrent operations to our sync method like so:
…which then requires us to invoke the resume function, passing its result as the callback to our asynchronous operation: