{
    "componentChunkName": "component---src-templates-blog-post-tsx",
    "path": "/blog/2023-12-11-await-event-horizon/",
    "result": {"data":{"blogPost":{"title":"The await event horizon in JavaScript","slug":"/blog/2023-12-11-await-event-horizon/","authorNodes":[{"name":"Charles Lowell","slug":"/people/charles-lowell/"}],"markdown":{"html":"<p>There is a boundary around every black hole where the velocity required to escape its gravitational pull exceeds the speed of light. Once anything, including light itself, passes over that threshold, it is trapped inside the mysterious interior of the black hole forever. There is no escape, and there is no return back to the rest of the universe. This boundary is called the black hole’s event horizon.</p>\n<p>A similar boundary exists around every JavaScript Promise, and once the flow of execution crosses over it, there is no way to forcibly escape and return back from whence it came. I refer to this boundary as the Promise’s <code class=\"language-text\">await</code> event horizon.</p>\n<p>An async function traverses an <code class=\"language-text\">await</code> event horizon every time it pauses for the result of a Promise. When it does, control can never return back to it until such time as that Promise settles; and there is absolutely no way to guarantee that it (or any) Promise will ever settle. It might happen in the next tick of the event loop, or in the worst case scenario, it just might never settle at all. When that happens, the poor waiting function is stuck forever helpless… suspended like an insect in amber.</p>\n<p>This is not theoretical. It is of important, practical concern. For example, consider this async function that implements the very common pattern of doing some setup, performing an operation, and then doing necessary teardown. In the code below, the async function acquires a lock, awaits a function it receives as an argument, and then, once that operation is complete, releases the lock.</p>\n<div class=\"gatsby-highlight\" data-language=\"jsx\"><pre class=\"language-jsx\"><code class=\"language-jsx\"><span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">protect</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">work</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">let</span> lock <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">acquireLock</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">await</span> <span class=\"token function\">work</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span> <span class=\"token keyword\">finally</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token function\">release</span><span class=\"token punctuation\">(</span>lock<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>But what happens if the Promise returned by <code class=\"language-text\">work()</code> never settles? The answer is that the <code class=\"language-text\">protect()</code> function has passed over the <code class=\"language-text\">await</code> event horizon, never to resume. As a result, the lock it acquired at its start is never released, and so we say the lock has been “leaked”.</p>\n<blockquote>\n<p>Resource leaks are considered by many to be the most insidious category of bug in software because of the difficulty in tracking them down coupled with the fact that they often lay hidden until the system is under the stress of a heavy workload.</p>\n</blockquote>\n<p>Of course as a rule, most promises <em>do</em> settle if we let them run long enough and so for the most part we abide by the blissful convention that they <em>will</em>. But happy assumptions rarely pan out at scale. In fact, we find out pretty quickly that it isn’t just promises taking forever that are problematic.</p>\n<p>Let’s suppose that we wanted to call our <code class=\"language-text\">protect()</code> function from within a command line interface, and that when the user hits CTRL-C, we exit our process. If our work takes ten seconds, but the user hits CTRL-C after nine and a half seconds, then control never returns from beyond the <code class=\"language-text\">await</code> event horizon and the lock is leaked once again.</p>\n<p>We aren’t talking about a promise that never settles here. We’re talking about one that would have settled imminently, and yet it only took a difference of five hundred milliseconds for the leak to manifest.</p>\n<h2 id=\"is-explicit-resource-management-a-solution\" style=\"position:relative;\"><a href=\"#is-explicit-resource-management-a-solution\" aria-label=\"is explicit resource management a solution permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Is Explicit Resource Management a solution?</h2>\n<p>In a word: no.</p>\n<p><a href=\"https://github.com/tc39/proposal-explicit-resource-management\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Explicit resource management</a>, a stage 3 TC39 proposal as of this writing, allows you to bundle setup and teardown code together. It saves you from going through the ceremony of a  <code class=\"language-text\">try/catch</code> block and makes it much more difficult to inadvertantly leak resources. If our hypothetical locking mechanism had it built in, it would allow us to write our <code class=\"language-text\">protect()</code> function much more clearly.</p>\n<div class=\"gatsby-highlight\" data-language=\"jsx\"><pre class=\"language-jsx\"><code class=\"language-jsx\"><span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">protect</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">work</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  using lock <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">acquireLock</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">await</span> <span class=\"token function\">work</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>When <code class=\"language-text\">protect()</code> finishes and <code class=\"language-text\">lock</code> passes out of scope, it is automatically released according to its pre-bundled deallocation logic.</p>\n<p>While this a handy improvement, it does nothing to change the fundamental physics of the <code class=\"language-text\">await</code> event horizon. Once flow control passes through it, there is no coming back until <code class=\"language-text\">work()</code> settles. If that takes too long, then <code class=\"language-text\">protect()</code> will take too long, and as a result, the automatic destruction of the <code class=\"language-text\">lock</code> resource will never be triggered. In other words, it is leaked.</p>\n<h2 id=\"does-abortsignal-help\" style=\"position:relative;\"><a href=\"#does-abortsignal-help\" aria-label=\"does abortsignal help permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Does AbortSignal help?</h2>\n<p>In theory yes, but in actual practice kinda-not-really. Abort signals can, when applied with discipline, remediate the problem somewhat, but they cannot solve it.  It begins with the reality that there is no agreed upon way to “cancel” a promise when handed an <code class=\"language-text\">AbortSignal</code>. In fact, it is a very thorny issue which caused T39 to <a href=\"https://github.com/tc39/proposal-cancelable-promises/issues/70\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">throw up its hands almost a decade ago</a>, and if you think you know the answer offhand, then the odds are that you haven’t <a href=\"https://news.ycombinator.com/item?id=13214487\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">thought about it long enough</a>. However if we must, one way is to wrap every promise in a <code class=\"language-text\">safe()</code> function that serves as a barrier to protect us from the <code class=\"language-text\">await</code> event horizon.</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">safe</span><span class=\"token punctuation\">(</span>promise<span class=\"token punctuation\">,</span> signal<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> <span class=\"token builtin\">Promise</span><span class=\"token punctuation\">.</span><span class=\"token function\">race</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">[</span>\n    promise<span class=\"token punctuation\">,</span>\n    <span class=\"token keyword\">new</span> <span class=\"token class-name\"><span class=\"token builtin\">Promise</span></span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>_<span class=\"token punctuation\">,</span>reject<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> signal<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"abort\"</span><span class=\"token punctuation\">,</span> reject<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>If <code class=\"language-text\">signal</code> fires before <code class=\"language-text\">promise</code> settles, then our safe function will immediately reject, returning control to the caller by throwing an error. With this mechanism in hand, we can re-write our <code class=\"language-text\">protect()</code> function to thread an abort signal throughout the entire computation.</p>\n<div class=\"gatsby-highlight\" data-language=\"jsx\"><pre class=\"language-jsx\"><code class=\"language-jsx\"><span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">protect</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">work<span class=\"token punctuation\">,</span> signal</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">let</span> lock <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">safe</span><span class=\"token punctuation\">(</span><span class=\"token function\">acquireLock</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> signal<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">await</span> <span class=\"token function\">safe</span><span class=\"token punctuation\">(</span><span class=\"token function\">work</span><span class=\"token punctuation\">(</span>signal<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> signal<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span> <span class=\"token keyword\">finally</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token function\">release</span><span class=\"token punctuation\">(</span>lock<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The function now uses <code class=\"language-text\">safe()</code> to wrap every <code class=\"language-text\">await</code> expression in a circle of protection that prevents execution from becoming trapped beyond its event horizon. In addition, it passes <code class=\"language-text\">signal</code> down to <code class=\"language-text\">work()</code> so that it, and any functions that it calls can do the same. Now, when <code class=\"language-text\">signal</code> is fired, no matter if <code class=\"language-text\">work()</code> becomes stuck, our <code class=\"language-text\">protect()</code> function will exit, and the lock will be released. However, the approach still comes with this serious and unavoidable caveat: the abort signal is a hope, not a constraint.</p>\n<p>It’s not just that an extra abort signal is cumbersome to both use and pass around. It is. It’s that if <code class=\"language-text\">work()</code> or any of the functions that it calls, or any of the functions that <em>they</em> call, fail to use <code class=\"language-text\">signal</code> and <code class=\"language-text\">safe</code>() then we are right back in the same boat of having async functions in our call tree that become trapped beyond the <code class=\"language-text\">await</code> event horizon and leak resources as a result.</p>\n<p>What’s fundamentally missing is the power of abstraction: the freedom to think about <code class=\"language-text\">work()</code> as a black box and feel secure that it will return control to its caller whenever it needs it most, no matter how <code class=\"language-text\">work()</code> is constructed internally. Using abort signals, the only way to achieve this is would be to read its source code and the source code of all its transitive dependencies to ensure that they also observe abort signal discipline. In practice, nobody would do that, and the scarcity of libraries that actually integrate abort signals shows that nobody does. While they can work around the obstacle through the imposition of discipline, they cannot make it disappear altogether. The fact remains that once control passes through the <code class=\"language-text\">await</code> event horizon, it cannot be brought back.</p>\n<h2 id=\"structured-concurrency-and-the-await-event-horizon\" style=\"position:relative;\"><a href=\"#structured-concurrency-and-the-await-event-horizon\" aria-label=\"structured concurrency and the await event horizon permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Structured Concurrency and the await event horizon</h2>\n<p>There are many remediations for the problem this poses, but most of them amount to some variation of the abort signal. The <code class=\"language-text\">await</code> event horizon however, remains axiomatic because the mechanics of it are baked into the heart of the runtime. There is no way to reliably pierce through it.</p>\n<p>The consequence of this is that the <em>minimum</em> lifetime of any given <code class=\"language-text\">async</code> function is determined solely by the lifetime of its <em>innermost</em> promises. In other words, the natural lifetime of an <code class=\"language-text\">async</code> function is determined from the inside out.</p>\n<p><code class=\"language-text\">protect()</code> cannot continue until <code class=\"language-text\">work()</code> settles… whenever that may be. So it is <code class=\"language-text\">work()</code> that determines when <code class=\"language-text\">protect()</code> can continue, and it is <code class=\"language-text\">protect()</code> that determines the natural lifetime of our main function. Anything that overruns the exit of our process is in the “danger zone” of being leaked. This includes our hypothetical lock</p>\n<p><figure class=\"figure\"><img src=\"/img/2023-12-11-await-event-horizon/leak-zone.png\"><figcaption class=\"figure-caption\">Any code that runs longer than needed is in danger of being leaked</figcaption></figure></p>\n<p>In fact, this is precisely the opposite of what is required by a Structurally Concurrent system. Namely, that the <em>maximum</em> lifetime of a function is constrained by the lifetime of the function that calls it. Instead of waiting around for async operations to complete that have no bearing on the outcome of a computation, a structurally concurrent system will return from those functions immediately the moment they are no longer necessary. In our example of the command line interface, as soon as the user hits ctrl-c, everything else becomes immediately irrelevant.</p>\n<p>What we would like to see in this case is the forcible return of control <em>back</em> to the <code class=\"language-text\">protect()</code> function, so that it can run its <code class=\"language-text\">finally</code> block so that the lock is not leaked and the process exits gracefully.</p>\n<p><figure class=\"figure\"><img src=\"/img/2023-12-11-await-event-horizon/graceful-shutdown.png\"><figcaption class=\"figure-caption\">A well behaved operation always returns</figcaption></figure></p>\n<p>However, in order to enforce the shutdown of these irrelevant functions, there must be some mechanism by which to impose a return of control from the top down. But we’ve just seen how in <code class=\"language-text\">async</code> functions, once control passes through the <code class=\"language-text\">await</code> event horizon, it cannot be brought back. It’s for this reason that primitives based on <code class=\"language-text\">async</code> functions can at best hope for structured concurrency, but they can never guarantee it.</p>\n<h2 id=\"structured-concurrency-and-javascript\" style=\"position:relative;\"><a href=\"#structured-concurrency-and-javascript\" aria-label=\"structured concurrency and javascript permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Structured Concurrency and JavaScript</h2>\n<p>You might be thinking at this point that structured concurrency in JavaScript is a lost cause because it can’t be achieved with <code class=\"language-text\">async</code> functions. Far from it! Structured Concurrency is not only possible, you can already find it out in the wild today in the likes of projects like <a href=\"https://frontside.com/effection\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Effection</a>, <a href=\"http://effects.js.org\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Effect-TS</a>, and <a href=\"https://github.com/neurosnap/starfx\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">StarFX</a> (to name a few). These libraries come in all shapes and sizes, but one thing that they all share in common is an embrace of <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">generator functions</a> as a core technique.</p>\n<p>This is because generators functions in JavaScript are not limited by the <code class=\"language-text\">await</code> event horizon. They represent full-fledged <a href=\"https://en.wikipedia.org/wiki/Delimited_continuation\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">delimited continuations</a>, which is one of the most (if not the most) powerful flow control primitive there is. Without delving into what delimited continuations are, suffice it to say that they can be used to express <em>any</em> other control mechanism you’d care to implement, from <code class=\"language-text\">while</code> loops, to <code class=\"language-text\">try/catch</code> blocks to algebraic effect handlers. In fact, in their essence, <code class=\"language-text\">async</code> functions themselves are just a watered down version of generator functions limited to the specific domain of promise handling.</p>\n<p>Critically for our use-case, generator functions allow for an explicit <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><code class=\"language-text\">return()</code></a> which signals them to exit <em>posthaste</em> from wherever they are currently executing. Yet as they do, they will still follow critical code paths such as <code class=\"language-text\">finally {}</code> blocks or explicit resource methods.</p>\n<p>Perhaps someday in the distant future, <code class=\"language-text\">async</code> functions will provide the programmer with a mechanism to escape the <code class=\"language-text\">await</code> event horizon, but until that time, a structured concurrency model based on <code class=\"language-text\">async/await</code> will be nothing more than science fiction.</p>","frontmatter":{"date":"December 11, 2023","description":"Why async functions in JavaScript are insufficient as a Structured Concurrency primitive","tags":["javascript","structured concurrency"],"img":{"childImageSharp":{"fixed":{"src":"/static/f41050ac2ccba19fd9dd0c46f90fc75f/db955/2023-12-11-await-event-horizon.png"}}}}}}},"pageContext":{"id":"23f82221-cf1e-58a6-a6a7-42cf13929228"}},
    "staticQueryHashes": ["1241260443"]}