|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- title: Why I’m Not Waiting On ‘await’ (part 2)
- url: https://blog.getify.com/not-awaiting-2
- hash_url: dc98d4cae65338a1be618bab8fa43177
-
- <div>
- <p>Two-part series on the highs and lows of the coming <code>async..await</code> in JS:</p>
- <ul>
- <li><a href="https://blog.getify.com/not-awaiting-1">Part 1</a>: what <code>async..await</code> is and why it’s awesome</li>
- <li><a href="https://blog.getify.com/not-awaiting-2">Part 2</a>: why <code>async..await</code> falls short compared to generators+promises</li>
- </ul>
- </div>
- <hr/>
- <hr/>
- <p>In <a href="https://blog.getify.com/not-awaiting-1/">part one</a>, I covered why <code>async..await</code> has built up such momentum on the hype train. The promise of sync-looking async is powerfully attractive.</p>
- <p>Now I’ll cover why I think we should slow that train down. A bunch.</p>
- <p>I’m going to make a case for sticking with generators+promises by exploring some shortcomings of <code>async..await</code>. As I’ve already shown, the syntax of generators+promises isn’t that much worse. But they’re more powerful.</p>
- <h3 id="missing-direct-delegation">Missing Direct Delegation</h3>
- <p><a href="https://blog.getify.com/not-awaiting-1/#a-rose-by-any-other-name">Remember in part one</a> where I asserted that <code>yield</code> is more about control transfer than value semantics? That’s more obvious once we look at <code>yield \*</code>.</p>
- <p>In a generator, a <code>yield</code> transfers control back to the calling code. But what if we want to delegate to another piece of code, while still preserving the pause/resume async semantics.</p>
- <p>We can instead do a <code>yield \*</code> delegation to another generator, like this:</p>
- <pre class="code">
- function \*foo() {
- var x = yield getX();
- var y = yield \* bar( x );
- console.log( x, y );
- }
-
- function *bar(x) {
- var y = yield getY( x );
- var z = yield getZ( y );
- return y + z;
- }
-
- run( foo() );
- </pre>
- <p>You'll notice that the <code>yield \* bar(..)</code> lets the <code>bar()</code> generator run. But <strong>how</strong> does it run? We didn't use the <code>run(..)</code> utility on that call.</p>
- <p>We don't have to, because <code>yield \*</code> actually <em>delegates control</em>, so the original <code>run(..)</code> from the last line of the snippet is now transparently controlling (pausing/resuming) the <code>bar()</code> generator as it proceeds, without even knowing it. Once <code>bar()</code> completes, its return value (if any) finally resumes the <code>yield \*</code> with that value, returning execution to the <code>foo()</code> generator.</p>
- <p>The <code>yield \*</code> can even be used to create a sort of generator recursion, where a generator instance delegates to another instance of itself.</p>
- <p>Because <code>async function</code>s are not about control transfer, there's no equivalent to <code>yield \*</code> like <code>await \*</code>. There <em>was</em> talk about appropriating the <code>await \*</code> syntax to mean something like <code>await Promise.all(..)</code>, but that was eventually rejected.</p>
- <p>The <code>async function</code> equivalent would be:</p>
- <pre class="code">
- async function foo() {
- var x = await getX();
- var y = await bar( x );
- console.log( x, y );
- }
-
- async function bar(x) {
- var y = await getY( x );
- var z = await getZ( y );
- return y + z;
- }
-
- foo();
- </pre>
- <p>We don't need a <code>yield \*</code>-type construct here because <code>bar()</code> creates a promise we can <code>await</code>.</p>
- <p>That may seem that it's basically just, again, nicer syntax sugar removing the need for the <code>*</code>. But it's subtly different. With the <code>yield \*</code>, we're not creating a wrapper promise but rather just passing control directly through.</p>
- <p>What if <code>bar()</code> has some case where it can provide its result right away, such as pulling from a cache/memoization?</p>
- <pre class="code">
- function \*bar(x) {
- if (x in cache) {
- return cache[x];
- }
-
- var y = yield getY( x );
- var z = yield getZ( y );
- return y + z;
- }
- </pre>
- <p>versus:</p>
- <pre class="code">
- async function bar(x) {
- if (x in cache) {
- return cache[x];
- }
-
- var y = await getY( x );
- var z = await getZ( y );
- return y + z;
- }
- </pre>
- <p>In both cases, we immediately <code>return cache[x]</code>.</p>
- <p>In the <code>yield \*</code> case, <code>return</code> immediately terminates the generator instance, so we'd get that result back in <code>foo()</code> right away. But with promises (which is what <code>async function</code>s return), unwrapping is an async step, so we have to wait a tick before we can get that value.</p>
- <p>Depending on your situation, <strong>that may slow your program down a little bit</strong>, especially if you're using that pattern a lot.</p>
- <h3 id="cant-be-stopped">Can't Be Stopped</h3>
- <p>In part one, <a href="https://blog.getify.com/not-awaiting-1/#a-rose-by-any-other-name">I asserted</a> that <code>await</code> is declarative while <code>yield</code> is imperative. Pushing that to a higher level, <code>async function</code>s are declarative and generators are imperative.</p>
- <p>An <code>async function</code> is represented by (ie, returns) a promise. This promise represents one of two states: the whole thing finished, or something went wrong and it stopped. In that sense, the <code>async function</code> is an all-or-nothing proposition. But moreover, because it's a promise that's returned, all you can do is observe the outcome, not control it.</p>
- <p>By contrast, a generator is represented by (ie, returns) an iterator. The iterator is an external control for each of the steps in the generator. We don't just observe a generator's outcome, we participate in it and control it.</p>
- <p>The <code>run(..)</code> function that drives the generator+promise async flow control logic is actually negotiating via the iterator.</p>
- <p>The <code>next(..)</code> command on the iterator is how values are sent into the generator, and how values are received from it as it progresses. The iterator also has another method on it called <code>throw(..)</code>, which injects an exception into the generator at its paused position; <code>next(..)</code> and <code>throw(..)</code> pair together to signal (from the outside) success or failure of any given step in the generator.</p>
- <p>Of course, there's no requirement that the code controlling a generator's iterator choose to keep it going with <code>next(..)</code> or <code>throw(..)</code>. Merely opting to stop calling either has the effect of indefinitely pausing the generator. The iterator instance could be left in this paused state until the program ends or it's garbage collected.</p>
- <p>Even better, a generator's iterator has a third method on it that can be useful: <code>return(..)</code>. This method terminates the generator at its current paused point; you can send an affirmative signal into the generator from the outside telling it to stop right where it sits.</p>
- <p>You can even detect and respond to this <code>return(..)</code> signal inside the generator with the <code>finally</code> clause of a <code>try</code> statement; a <code>finally</code> will always be executed, even if the statement inside its associated <code>try</code> was terminated from the outside with an iterator's <code>return(..)</code> method.</p>
- <p>A <code>run(..)</code> that was slightly smarter could offer the advantage of allowing a generator to be stopped from the outside.</p>
- <p>Imagine a scenario where midway through a generator's steps, you can tell by the values emitted that it's appropriate to stop the progression of the generator -- to cancel any further actions. Maybe the generator makes a series of Ajax calls one after the other, but one of the results signals that none of the others should be made.</p>
- <p>Consider a more capable <code>run(..)</code> used like this:</p>
- <pre class="code">
- run( main(), function shouldTerminate(val){
- if (val == 42) return true;
- return false;
- } );
- </pre>
- <p>Each time a value is ready to be sent in to resume the <code>main()</code> generator, this <code>shouldTerminate(..)</code> function is first called with the value. If <em>it</em> returns <code>true</code>, then the iterator is <code>return(..)</code>d, otherwise the value is sent via <code>next(..)</code>.</p>
- <p>A generator provides the power of external cancelation capability whereas an <code>async function</code> can only cancel itself. Of course, the necessary cancelation logic can be baked into the <code>async function</code>, but that's not always appropriate.</p>
- <p>It's often much more appropriate to separate the async steps themselves from any logic that will consume or control (or cancel) them as a result. With generators that is possible, but with <code>async function</code>s it's not.</p>
- <h3 id="scheduling-is-hidden">Scheduling Is Hidden</h3>
- <p>It's tempting to assume that a program should always complete its async steps ASAP. It may be hard to conceive of a situation where you wouldn't want some part of the program to proceed <em>right away</em>.</p>
- <p>Let's consider a scenario where we have more than one set of asynchronous logic operating at the same time. For example, we might be making some HTTP requests and we might also be making some database calls. For the sake of this discussion, let's assume that each set of behavior is implemented in its own <code>async function</code>.</p>
- <p>Because <code>async function</code>s essentially come with their own <code>run(..)</code> utility built-in, the progression of steps is entirely opaque and advances strictly in ASAP fashion.</p>
- <p>But what if you needed to control the scheduling of the resumptions of each task's steps? What if, for the purposes of throttling resource usage, you needed to make sure that no more than one HTTP request/response was handled for each database request?</p>
- <p>Another problem could be that one set of async steps might have each step proceed so quickly (essentially immediately) that this task could "starve" the rest of the system by not letting any other tasks have a chance. To understand this possibility, you should read more about <a href="https://github.com/getify/You-Dont-Know-JS/blob/master/async%20&%20performance/ch1.md#jobs">microtasks (aka Jobs)</a>. Basically, if each step in a promise chain resolves immediately, each subsequent step is scheduled "right away", such that no other waiting asynchronous operations get to run.</p>
- <p>Or what if you had ten sets of HTTP request/response cycles, but you need to make sure that no more than 3 of them were being processed at any given moment? Or perhaps you needed to not proceed ASAP but rather in a strictly round-robin order through the set of running tasks.</p>
- <p>There's probably half a dozen other scenarios where naive scheduling of the progression of async operations is not sufficient. In all those cases, we'd like to have more control.</p>
- <p>An <code>async function</code> affords no such control because the promise-resumption step is baked into the engine. But a smarter <code>run(..)</code> utility driving your generators could absolutely selectively decide which ones to resume in which order.</p>
- <p>By the way, while I'm being general about such scenarios for brevity sake, this is not purely theoretical. An open area of my research in async programming is <a href="..">CSP</a>, Communicating Sequential Processes, the model for concurrency used in the <em>go</em> language and also in ClojureScript's <code>core.async</code>.</p>
- <p>Normally CSP processes are modeled in JS using generators, because you will in general have many of them running at the same time, and usually need fine grained control over which ones resume after others pause. Attempts have been made to model CSP using <code>async function</code>s, but I believe these systems are inherently flawed in that the library is susceptible to all the above limitations because it has no control over the scheduling.</p>
- <p>CSP is one very concrete example of why it's important to have scheduling control over async operations. <code>async function</code>s just don't offer what we need, but generators do.</p>
- <h3 id="async-functions-vs-generators"><code>async function</code>s vs Generators</h3>
- <p>OK, so let's take stock of where we're at.</p>
- <p>First we examined how <code>async function</code>s work and what makes them so nice from a syntactic sugar perspective. But now we've seen several places where the tradeoff is that we lose control of potentially very important aspects of our asynchrony.</p>
- <p>You may consider these limitations unimportant because you haven't needed to do any of the fine-grained control described here. And that's fine. The <code>async function</code> will serve you just fine.</p>
- <p>But... what pain are you <em>really</em> enduring if you stuck with generators? Again, a side-by-side comparison:</p>
- <pre class="code">
- function \*foo(x) {
- try {
- var y = yield getY( x );
- return x + y;
- }
- catch (err) {
- return x;
- }
- }
-
- run( foo( 3 ) )
- .then( function(result){
- console.log( result );
- } );
- </pre>
- <p>versus:</p>
- <pre class="code">
- async function foo(x) {
- try {
- var y = await getY( x );
- return x + y;
- }
- catch (err) {
- return x;
- }
- }
-
- foo( 3 )
- .then( function(result){
- console.log( result );
- } );
- </pre>
- <p>They're almost identical.</p>
- <p>In both versions you get localized synchronous semantics (including pause/resume and error handling), and you get a promise that represents the completion of the <code>foo(..)</code> task.</p>
- <p>For generators, you <em>do</em> need the simple <code>run(..)</code> library utility, whereas with <code>async function</code>s, you don't. But as we discussed, such a utility is only about a dozen lines of code in its simplest form. So is it actually that burdensome?</p>
- <p>For the extra control we get, I think <code>run(..)</code> is an easy price to justify.</p>
- <h3 id="finally"><code>finally { .. }</code></h3>
- <p>For the majority of my async code, I believe generators+promises is a better, more flexible, more powerful option, at only a minimal expense.</p>
- <p>I'd rather not start with <code>async function</code>s and then later realize that there are places where I need more control, and have to refactor back to generators. That's especially true considering the <a href="https://blog.getify.com/not-awaiting-1/#just-use-both">complications of mixing the two forms</a> that I addressed in part one.</p>
- <p><code>async..await</code> seems like a great feature. But it's too simplistic, IMO. The small syntactic benefits are not worth the loss of control.</p>
|