Does async/await solve a real problem?

# Jeswin Kumar (10 years ago)

Looking at my project (in which asynchronous calls are entirely done via generators), I can't see how async/await would simplify code for end-users like me (application programmers).

End users write the spawn()/Q.async()/co() wrapper at most one single time in an application:

  1. When using a framework like say koajs, you don't have to write it even once.
  2. While not using a framework, you'd have to use the wrapper one single time in say, the main.js file.

To use the example at strawman:async_functions

async function chainAnimationsAsync(elem, animations) { CODE; }

is just

function chainAnimationsAsync*(elem, animations) { same CODE; }

when flow control is done by a framework or at the entry point to your application. spawn() isn't needed.

I can't see how this will reduce application's code even a little. So my question is, is async/await needed?

One more question

  1. yield is practically very difficult to use in a project because you don't get proper stack traces (at least with the current flow control libraries). You'd only see the last call which threw the error, and then functions from the flow control library immediately below that. I suppose the generators leading up to the erring generator are all suspended and wouldn't be on the stack frame chain.

  2. yield* generator delegation solves this problem, you get real stack traces. I was able to get full stack traces simply by replacing all yield X() with yield* X()

example code as in: chopachom/syncio

So if there are valid use-cases for adding async/await to JS, shouldn't it be based on how yield* works rather than yield?

# Dean Landolt (10 years ago)

Yes, async/await solves one problem in particular that generators alone cannot—the ability to await some asynchronous value from within a true generator (one yielding actual values, not a coroutine yielding promises or thunks into a trampoline). These coro trampolines are a clever hack, but the async/await syntax allows these two completely distinct language features to become truly orthogonal. This is important in its own right, but also has immediately utility (e.g. more elegant lazy streams using for-of iteration).

# Domenic Denicola (10 years ago)

There are several problems solved by async/await instead of twisting generators:

  1. What if you wanted to use generators for lazy sequences (iterables), instead of asynchronicity? If your framework assumes all generators are for async, you lose the original use case of generators.

  2. Say what you mean. function* and yield mean something very different from async function and await, similar to how Subclass.prototype = Object.create(Superclass.prototype); Subclass.prototype.constructor = Subclass is different from class Subclass extends Superclass.

  3. Operator precedence. You can do await a + await b to mean (await a) + (await b), but yield a + yield b means yield (a + (yield b)).

  4. Ability to produce promise-returning functions without buying into a specific framework that interprets generators in a certain way. E.g., you could use async function f() { return 5; } to return a promise for 5, which people can consume with f().then(v => ...). If you try to do function* f() { return 5; } you will get an iterable, which is not understood to be asynchronous. (Hopefully my use of return 5 for brevity instead of more complex code does not confuse this point for you.)

As for stack traces, long stack trace support is a debugging feature, and the fact that yield* gets them right now doesn't mean that await won't get them in the future.

# Kevin Smith (10 years ago)

Also, see lukehoban/ecmascript-asyncawait#14 for previous discussion.

# Florian Bösch (10 years ago)

await has also another problem in that if somewhere, deep down the call stack, something is intending to do async, then up the entire call stack everywhere you've got to insert await. It's a bit of a headache for code maintenance (hello bicycle repair man jam session), and it's also fairly unfriendly for library authors.

There is a solution to that problem, which is not using generators if you'd want co-routines. If you want co-routine like behavior, please implement co-routines (and you can prop whatever scheduling/managing on top of that).

An argument has been made in earlier discussions on that topic, that JS VMs can't deal with co-routines (garbage collection, DOM, whatever). But surely, if the VM can support generators/continuations, it could support full co-routines.

I'd recommend python's greenlet as an outstanding implementation of a co-routine interface that includes basically everything one could wish from it. I'd not consider python 3's "asyncio" a proper co-routine implementation (i.e. it's the same generator/await hack as is being discussed here).

# Mark S. Miller (10 years ago)

VM issues are not the argument against coroutines or deep generators. The issue is that unpredictable interleaving makes reasoning about invariants much too difficult. Without these, we have an important guarantee: When f synchronously calls g, the only side effects that might have occurred by the time g returns to f are those g might have caused. Thus, these are the only side effect possibilities that f must worry about.

See section 18.3 of www.erights.org/talks/thesis/markm-thesis.pdf and replace postfix diagonal uparrow with prefix "await". In ES7 the example would be

async function foo() { return bar(await getint(), y()); }
... await foo() ...

The net effect is like co-routines, except that the placement of "async" and "await" -- like the diagonal uparrow in the text -- marks the places where interleaving might occur. This is as close to coroutine support as we should ever come.

# Florian Bösch (10 years ago)

A -> B -> C -> D -> E changes to

A -> B -> C -> D -> async E and causes

A await -> B await -> C await -> D await -> async E

And of course if A, B, C or D is used anywhere else it percolates trough the entire call graph.

Trying to protect people from interlaved code execution effects is noble. But doing so by introducing a rote thing to type everytime you change the code somewhere underneath is wrong. It's wrong because it breaks logic isolation, it becomes impossible to change part of the library/utiity code without this change affecting all code that uses it. This guarantees that it doesn't happen in practice, because it's too painful to do. It requires the code, that uses other code, to know about the internal behavior of that code.

If say, I'd propose a semantic that required you to write "foo" in the code, but just for those pieces of code that contain mentions of "bar", or that reference code that contains "bar" to the Nth degree, you'd accuse me of trying to introduce a purposefully unusable feature. How is await/async not an unusable feature?

# Florian Bösch (10 years ago)

Furthermore, since await is call graph infectious, for those really wanting to use it, it means that before long, await is written before every single function call. Which makes no sense.

# Mark S. Miller (10 years ago)

On Thu, Sep 11, 2014 at 7:22 AM, Florian Bösch <pyalot at gmail.com> wrote:

A -> B -> C -> D -> E changes to

A -> B -> C -> D -> async E and causes

A await -> B await -> C await -> D await -> async E

And of course if A, B, C or D is used anywhere else it percolates trough the entire call graph.

Trying to protect people from interlaved code execution effects is noble.

Exactly. Good to see we agree on the implications even if we value these outcomes differently.

But doing so by introducing a rote thing to type everytime you change the code somewhere underneath is wrong. It's wrong because it breaks logic isolation, it becomes impossible to change part of the library/utiity code without this change affecting all code that uses it. This guarantees that it doesn't happen in practice, because it's too painful to do. It requires the code, that uses other code, to know about the internal behavior of that code.

If say, I'd propose a semantic that required you to write "foo" in the code, but just for those pieces of code that contain mentions of "bar", or that reference code that contains "bar" to the Nth degree, you'd accuse me of trying to introduce a purposefully unusable feature. How is await/async not an unusable feature?

On Thu, Sep 11, 2014 at 7:25 AM, Florian Bösch <pyalot at gmail.com> wrote:

Furthermore, since await is call graph infectious, for those really wanting to use it, it means that before long, await is written before every single function call. Which makes no sense.

In a purely functional system without side effects of any form (i.e., ignoring even strictness and exceptions), interleaving is harmless, so this conclusion is valid. You would indeed safely get better code reuse if you placed async/await everywhere, so that instead should have been the default in such a language. In fact, since interleaving is harmless, you can schedule all computation as you like, including in parallel, lazily, eagerly, whatever, without harming correctness. In the absence of side effects, we should indeed not have placed a notational burden on interleaving points.

But in an imperative system with synchronous side effects that co-exists with interleaving hazards, we also needs a way to protect against such hazards. The economy of the communicating event loop model is that each turn is implicitly a mutually exclusive atomic transaction -- without needing extra notation for synchronization or transaction boundaries.

The cost of making atomicity cheap is that interleaving points must be made explicit. With callbacks, this cost is quite high. Promises reduce this cost substantially. async/await further reduces this cost about as far as it can be reduced, while still leaving an explicit marker.

# Florian Bösch (10 years ago)

The problem of code interleaving isn't on a fundamental level (like with threads). Threads are a very different best, that interlave at random points in time, and hence require some heavy lifting by the environmnet/OS to not immediately fall flat.

Cooperative multitasking between I/O primitives is much friendlier. But still, you can wreck some havoc if you have multiple tasklets running that modify the same data structure. You won't get a race condition in the classical sense, but you can still produce garbage data (like say call out to async inside a loop over an array of which you've cached the length).

However, the exact same breakage applies to await/async, because if you await inside a loop, of which you've cached the length, and have some other code modify the array in the meantime... So not really an argument.

# Jeff Morrison (10 years ago)

On 9/11/14, 10:22 AM, Florian Bösch wrote:

A -> B -> C -> D -> E changes to

A -> B -> C -> D -> async E and causes

A await -> B await -> C await -> D await -> async E

And of course if A, B, C or D is used anywhere else it percolates trough the entire call graph.

Sort of true, but this is no worse than the status quo.

function A() { return B(); }
function B() { return C(); }
function D() { return D(); }
function E() { return 42; } -> function E() {  return new 

Promise(function(res, rej) { res(42); }); }

I said "sort of" above because it's also worth pointing out that, in some cases, outer plain functions can still consume async functions without needing to themselves become async. The case arises where the result of the outer plain function doesn't depend on the async result of the inner function. Many cases don't apply here, but it's worth pointing out to show that consumption of async functions isn't always stack-viral:

function add(a, b) {
   var logSuccess = log('adding ' + a + ' and ' + b);
   if (!logSucces) {
     console.warn('log failed!');
   }
   return a + b;
}

function log(msg) {
   return logger.syncAppend(msg);
}

changes to

function add(a, b) {
   log('adding ' + a + ' and ' + b).catch(function(res) {
     console.warn('log failed!');
   }).done();

   return a + b;
}

async function log(msg) {
   return logger.asyncAppend(msg);
}
# Bruno Jouhier (10 years ago)

I'd like to inject my own experience into this discussion.

I am the author of streamline.js, a smalll extension to JavaScript which adds async/await semantics to the language. I developed this language extension in January 2011 and we have been using it since then in a large project (new web stack for a commercial ERP product). Before, in the early days of the project, we had been using raw callbacks and promise libraries.

The syntax of streamline.js is different from async/await but the semantics are aligned. The marker is different (_ instead of await) but it is explicit at all levels. Parallelism is achieved with futures, which are just simplified promises (the language was made promise friendly recently).

My experience is that async/await makes a huge difference for large projects with a lot of logic sitting on top of async APIs. More specifically:

  • Productivity: developers write code faster and they write less code.
  • Maintainability: code is easier to read and understand. Control flow is not disrupted by callbacks. Refactoring and modification are easy.
  • Robustness: Error handling, resource cleanup and invariant restoring can be done in a simple and reliable way with.well known constructs (try/catch and try/finally). Developers don't make silly mistakes like forgetting a callback or calling it twice.
  • Learnability: developers coming from other languages (Java, C#) can be brought up to speed quickly.
  • Comprehensive parallelization: async/await blends well with futures/promises. Concurrency problems are easy to analyze because control flow is natural and the markers highlight all the points where the flow may yield control.
  • TLS support: makes it easy to maintain a localization/security context across async calls.

So, from my experience, async/await is an absolute must for projects like ours.

On the syntax side, I am not convinced by the await construct. The problem is that await is prefix. So it does not play well in chains of async calls. Consider the following:

  • streamline: f1().f2().f3(_)
  • async/await: await (await (await f1()).f2()).f3();

I'm not particularly attached to the streamline syntax (reserving an identifier was an easy way to remain compatible with existing language tools: editors, linters, syntax coloring, ...). I would rather go with something like the infix ! proposal of strawman:concurrency.

I think that Mark Miller nails it really well:

The cost of making atomicity cheap is that interleaving points must be made explicit. With callbacks, this cost is quite high. Promises reduce this cost substantially. async/await further reduces this cost about as far as it can be reduced, while still leaving an explicit marker.

IMO promises are only a step forwards; async/await is a leap forwards.

Bruno

# C. Scott Ananian (10 years ago)

On Thu, Sep 11, 2014 at 4:41 PM, Bruno Jouhier <bjouhier at gmail.com> wrote:

I think that Mark Miller nails it really well:

The cost of making atomicity cheap is that interleaving points must be made explicit. With callbacks, this cost is quite high. Promises reduce this cost substantially. async/await further reduces this cost about as far as it can be reduced, while still leaving an explicit marker.

IMO promises are only a step forwards; async/await is a leap forwards.

And, at the risk of banging my own drum -- async/await pave the way for some really interesting work with optimistic transactional locking and speculative execution, since they are (as noted in this thread) explicit markers of atomicity boundaries.

# Bruno Jouhier (10 years ago)

I forgot to mention performance.

streamline has been an interesting playground to test performance because it supports 3 different backends:

  • pure callbacks with a sophisticated CPS transform
  • fibers (much simpler transform)
  • generators (transform is similar to fibers, runtime is a bit trickier).

Pure callbacks are the fastest when the execution stacks are shallow and the logic is simple.

When the call stacks get deep and/or the logic gets complex, fibers win. This is because we have several layers of async functions calling async functions before we hit low level callback APIs. The intermediate async calls are executed as normal calls with normal returns (no callback, no allocation of a generator object) until we reach the low level callback APIs (where a Yield pops up). With some code patterns like caching (where only the first call is truly async) the fibers backend can be order of magnitudes faster than callbacks (even handcrafted ones).

Generators always lag behind (but they have more regular memory patterns than fibers). Part of this poor performance may also be due to lack of optimization in V8.

If async/await is baked into the language VM implementers will be able to choose between different strategies: callbacks, single frame continuations, deep continuations, ... to optimize the code. The nice thing is that the await markers will always be there on the surface, even if the VM takes some shortcuts below.

Bruno

2014-09-11 22:48 GMT+02:00 C. Scott Ananian <ecmascript at cscott.net>:

# C. Scott Ananian (10 years ago)

On Thu, Sep 11, 2014 at 5:27 PM, Bruno Jouhier <bjouhier at gmail.com> wrote:

If async/await is baked into the language VM implementers will be able to choose between different strategies: callbacks, single frame continuations, deep continuations, ...

...and transactional speculation. Or at least, that was the project I was hacking on in Rust/Turtlescript for a while (cscott/rusty-turtle) when I was fishing for a job at Mozilla.

The idea is that you actually execute all ready-to-execute tasks in (true multithreaded, multiprocessor) parallel, relying on the programmer's explicit atomicity boundaries and transactional memory to clean things up in order to preserve the appearance of atomic in-order execution.

# Tom Van Cutsem (10 years ago)

2014-09-11 16:22 GMT+02:00 Florian Bösch <pyalot at gmail.com>:

A -> B -> C -> D -> E changes to

A -> B -> C -> D -> async E and causes

A await -> B await -> C await -> D await -> async E

And of course if A, B, C or D is used anywhere else it percolates trough the entire call graph.

Trying to protect people from interlaved code execution effects is noble. But doing so by introducing a rote thing to type everytime you change the code somewhere underneath is wrong. It's wrong because it breaks logic isolation, it becomes impossible to change part of the library/utiity code without this change affecting all code that uses it. This guarantees that it doesn't happen in practice, because it's too painful to do. It requires the code, that uses other code, to know about the internal behavior of that code.

Arguably, the fact that a function may suspend in mid-flight should be part of its documentation (i.e. function signature), as it directly affects the caller.

As an analogy, consider IO in Haskell: if you have this large piece of purely functional code, and deep down you're all of a sudden introducing mutable state, you will need to refactor the larger piece of code to reveal that it in fact performs IO as well.

While this refactoring may be tedious (no disagreement there), it forces you to review all affected code, which is good, because the dependencies of that code may have changed (i.e. side-effect the client previously thought would execute atomically may now no longer be atomic).

# Florian Bösch (10 years ago)

On Fri, Sep 12, 2014 at 8:00 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

While this refactoring may be tedious (no disagreement there), it forces you to review all affected code, which is good, because the dependencies of that code may have changed (i.e. side-effect the client previously thought would execute atomically may now no longer be atomic).

It's often the case that the code which uses the async code, uses other code, which isn't authored by the author, and isn't documented for await compatibility. For instance:

$.each(somelist, async function(item){ await Foobar(item); });

Suppose $ is jQuery, is this async safe? Google for jQuery documentation and/or read code.

somelist.each(async function(item){ await Foobar(item); });

Has the browser made his each function async safe? Google for JS API documentation of the browser and/or start reading the source code of the browser (really?).

This is kind of a pervasive problem. It's not only that the refactoring/hunting for references is tedious. It's that sometimes, you might not have the plain source (some proprietary/minified/obfuscated library anybody?), or that the implementation is quite complex, and frankly, you just don't have the time to hunt around its millions of lines of code to figure out if it'll work.

Now to be fair, the same problem applies to co-routines as well. You can't know if a given piece of code is co-routine safe unless you dig it up. But at least it doesn't make you type forwards/backwards await/async each time you decide it didn't work out, trough hundreds, or thousands, or maybe even tens of thousands of lines. In fact, doing this will become an utter impossibility fairly quickly anyway, and the only way to do it would be some kind of automated tool that can insert the async/awaits for you trough the code (pray that it's inferrable, or you're screwed). Ohyeah, it'd be nice if that tool could insert some invisible code so you don't just clutter lines with stuff you don't hand-manage anyway. Wait, no, that was co-routines again wasn't it? silly me.

# Bruno Jouhier (10 years ago)

Arguably, the fact that a function may suspend in mid-flight should be part of its documentation (i.e. function signature), as it directly affects the caller.

It does. With async/await the function signature must be marked with 'async' if the function body contains 'await'.

As an analogy, consider IO in Haskell: if you have this large piece of purely functional code, and deep down you're all of a sudden introducing mutable state, you will need to refactor the larger piece of code to reveal that it in fact performs IO as well.

While this refactoring may be tedious (no disagreement there), it forces you to review all affected code, which is good, because the dependencies of that code may have changed (i.e. side-effect the client previously thought would execute atomically may now no longer be atomic).

It is tedious but not error prone. The compiler will tell you if you did anything wrong.

In a large project, high level code tends to be async anyway because there is always some async code (from which you need a result) like a database operation, a web service call, a file access, etc. underneath. Low level code is usually a mix of tight algorithms written in sync style and low level I/O written in async style. So the refactoring, when it happens, is usually contained to one algorithm, as the high level code is already written in async style.

Refactoring sync code into async is completely straightforwards with async/await. it is just a matter of adding async/await keywords. Without async/await you have to restructure the code with callbacks or promises which is a very different story: complex and error prone.

Also, an observation on explicit markers: these markers are very useful in low level I/O handling code because they let you clearly see the boundaries of the atomic sequences. In high level code, they are less important because the concurrency problems should be addressed at the low level and the high level code usually deals with data which is specific to a request and which is not shared with other requests.

When working with async/await you will also need a library function that lets you protect critical sections containing await markers. In our library, we do it with a funnel:

var fun = createFunnel(1);

async function foo() { var zoo = await fun(async function() { // critical section containing await directives } }

Funnels can also be used to limit the level of concurrency (fun = flows.funnel(max)). This is a very simple way to avoid resource exhaustion in high concurrency scenarios.

# C. Scott Ananian (10 years ago)

On Fri, Sep 12, 2014 at 2:24 AM, Florian Bösch <pyalot at gmail.com> wrote:

It's often the case that the code which uses the async code, uses other code, which isn't authored by the author, and isn't documented for await compatibility. For instance:

$.each(somelist, async function(item){ await Foobar(item); });

Suppose $ is jQuery, is this async safe? Google for jQuery documentation and/or read code.

I don't think this example is valid. As I understand it, async function is a function that returns a promise. You don't have to look into the library at all, it's just a normal value-returning function from its perspective. I think your example should have been something like:

await Promise.all($.map(somelist, async function(item) { await Foobar(item); }).

But I'm not 100% up-to-speed on the await/async proposal. Hopefully someone will correct me if I've gotten this wrong. The bottom line should be that atomicity boundaries are explicitly noted with await/async; for library code which does not expose an 'async' marker you are guaranteed that it will execute atomically. In this particular example this just means that all the promises created by the async function will be created atomically and execution returned to the called before any of the bodies of the async functions are run.

somelist.each(async function(item){ await Foobar(item); });

Has the browser made his each function async safe? Google for JS API documentation of the browser and/or start reading the source code of the browser (really?).

This is exactly the same as the jquery example. No implementation-introspection needed.

# Tab Atkins Jr. (10 years ago)

On Fri, Sep 12, 2014 at 8:14 AM, C. Scott Ananian <ecmascript at cscott.net> wrote:

This is exactly the same as the jquery example. No implementation-introspection needed.

You've got it right. async function solely makes the function return a Promise for its return value, rather than returning its return value normally; nothing magical goes on beyond that. There's absolutely no need to check if a function is "async-safe" or propagate an async on all functions up the call chain; you can just call the function normally and receive a Promise. You only need to make yourself async if you want to consume the value of an async function call directly, without having to go through the Promise indirection manually.

Browsers will likely optimize cases where an async function call is immediately consumed by an await expression and do some pipelining or something, but that's behind-the-scenes optimization.

# jordi baylina (10 years ago)

I propose that all the calls to any async function will be implicit (without the need of the await keyword). And precede it with a nowait only when you want a promise.

See this snipped proposal: gist.github.com/jbaylina/692d4e43329c8c0d22dd

This way, the code will not have the async clutter in the logic of the function, but will be need to be declared async advising the programmer that the code will not be executed atomically.

This proposal does not change the logics of the async/await proposal. It only change the way to call async functions. What is now implicit make them explicit with nowait and what is explicit with await make it implicit.

# Bergi (10 years ago)

jordi baylina schrieb:

I propose that all the calls to any async function will be implicit (without the need of the await keyword). And precede it with a nowait only when you want a promise.

This way, the code will not have the async clutter in the logic of the function, but will be need to be declared async advising the programmer that the code will not be executed atomically.

The problem with this is that the programmer cannot control those atoms. He only sees that the function is async, but not where the async flow starts, pauses, or does anything else. I don't understand whether your proposal only suggests to automatically await promises on function calls, or whether that should be done in every (sic!) single expression evaluation. The latter would be completely ridicolous, the former still be a hazard.

You really want to be explicit about yielding control. Everything else will call out for error-prone code, possibly breaking invariants that everyone expects to hold from looking at the code, just by random race conditions. You've never hunted a heisenbug, did you? And with your proposal, you could not trust any single function call - they might completely change their behaviour (becoming async) without you noticing.

The await keyword is definitely no "clutter".

Also have a look at this StackOverflow question&answer stackoverflow.com/a/25447289/1048572 where someone proposed a "use noblock" directive that would be very similar to your async functions. It has a nice example of such an unexpected race condition.

Kind , Bergi