node.js made highly concurrent servers easy by doing everything through non-blocking I/O, and the cost showed up as structure: every database lookup or API call added a callback level, and koush's own projects, built to hold tens of thousands of long-running connections, routinely ran seven or eight indents deep. A simple request-plus-query already needed two nested callbacks, error handling wrapped each level in its own try/catch, and serialized work such as paged API calls turned into recursive continuation functions.

The fixes available in 2011 moved the problem rather than removing it: the yield operator suspended and resumed iterators but stayed clunky, and source-to-source translators such as TameJS compiled clean-looking code into ordinary JavaScript that could run anywhere, at the price of debugging generated code instead of the code the developer wrote. Koush's reference point was C# 5's async/await, an explicit language feature that let asynchronous code read synchronously.

His move was to build that feature into the runtime: a fork of node.js with coroutines implemented in V8, an async function marker, and an await that terminates the function and resumes it when the callback completes. The parser still generated roughly the same AST as the callback version, so one try/catch spanned every await and a while loop could serialize paginated API requests, while ndb and node-inspector could still debug the original code. The price was a custom node build, which a team that controls its own servers could realistically pay.

Suspend the function on await via V8 coroutines so async code reads like sync code

Nested callbacks multiply with every I/O step and push error handling into separate scopes; await keeps one try/catch around a whole flow so exceptions cross callback boundaries like synchronous code.

Real coroutines in the runtime, not source-to-source translation, keep the code the developer writes as the code that runs, so ndb and node-inspector debug the original rather than generated output.

Sequential-looking control flow lets ordinary loops express serialized async work: the paged GDATA example replaces a recursive chunk function with a while loop.

Because a server team controls its deployment, forking node.js and V8 was a realistic price, and the design shipped as a working branch with tests rather than a proposal.

When workarounds push complexity into generated code or deeper nesting, fix the language level: suspend the whole function on I/O so readable code is what runs and what you debug

The project stayed a fork rather than a merged feature: the wiki, last edited in April 2013 after 25 revisions, framed a custom node build as a deployment option, and its TODO listed optimizing branches that contained await. Posted to Hacker News on 29 September 2011, it drew 222 points and 101 comments; readers called the approach very elegant and agreed the proper fix was to introduce coroutines. Nothing in the record shows the patch entering node.js; its visible effect was demonstrating, with working code, how JavaScript could get async/await at the language level.

FOLLOW THE EVIDENCE

The sources

  1. "async" support in node.js (koush/node wiki) github.com
  2. Fixing the callback spaghetti in node.js | Hacker News news.ycombinator.com