async/await -> await/async: a simpler, less error-prone async syntax

# Steven Mascaro (10 months ago)

Sorry for making this request now (rather than when async/await was first being formulated), but real-world use has led me to think some things could benefit from a slightly different syntax. I also apologise if this has been raised/discussed before, I wasn't able to find it. The proposal is most quickly explained with an example.

Current:

class RemoteService {
    async init() { ... }
    async setProp(id, val) { ...; return this }
    async getProp(id) { return ... }
    async runInBackground() { ... }
}

async function remoteExample() {
    let remote = new RemoteService();
    await remote.init();
    await (await remote.setProp('a', 1)).setProp('b', 2);
    remote.runInBackground();
    let val = await remote.getProp('a');
    return val;
}

Proposed:

class RemoteService {
    await init() { ... }
    await setProp(id, val) { ...; return this }
    await getProp(id) { return ... }
    await runInBackground() { ... }
}

await function remoteExample() {
    let remote = new RemoteService();
    remote.init();
    remote.setProp('a', 1).setProp('b', 2);
    async remote.runInBackground();
    let val = remote.getProp('a');
    return val;
}

Why:

Running things in a genuinely asynchronous way is actually unusual. The current async/await syntax (which I think should still stay) is the exact reverse of what fits the probable use cases. Missing the 'await' keyword (particularly on functions/methods that don't return a value) causes all sorts of hard to track down bugs. I myself didn't realise this until I (and others I work with) started making intense use of async/await.

The new proposed syntax above is obviously much simpler and would be very hard to get wrong. By contrast, the original syntax has proven surprisingly difficult to get consistently right. Even now with quite a lot of practice, I'm occasionally forgetting an await here and there. In addition, patterns like chaining get ugly very quickly. (Although I guess that could also be fixed with a method-friendly async call syntax.)

If the proposed syntax were available in addition to the current syntax, you could apply whichever keyword in the function declaration is most likely applicable at call sites. e.g. runInBackground could be declared 'async' rather than 'await', if you normally expect it to be run in the background.

# T.J. Crowder (10 months ago)

I understand the motivation, but I have to disagree with the proposal for multiple reasons:

  1. It adds too much confusion to the existing syntax.

  2. If the calling code needs to call one of these new-style await functions and access its promise (rather than awaiting it), it doesn't have a way to do that; async/await doesn't have full coverage of all patterns (yet, possibly never), sometimes you still need to access the actual promise (for instance, to feed into Promise.all or Promise.race). (Obviously you could add something to make it possible to access the promise.)

  3. Having a clear indicator in the source saying where the async boundaries are is useful for code correctness. With the new syntax, I have no idea of the temporal sequence of this code:

let x = foo();
let y = bar();

...without going and looking at the declarations of foo and bar. With current syntax, it's clear when there's an async break in the flow. I think the phrase is "prefer explicit to implicit" or something like that.

-- T.J. Crowder

# Naveen Chawla (10 months ago)

The crux of your idea is: let any calls of an async function inside an async function automatically await unless explicity instructed to run "in the background". I like the idea. I just wouldn't use the words "await" and "async" for it, because they are used as currently in C#.

On the last disagreement by T.J Crowder: 1. it's an alternative (perhaps with different words), not necessarily in conjunction with the existing syntax. 2. The promise would be accessible via e.g. const promise = background remote.runInBackground(); 3. If a presumption is made that in an async function, other async function calls are automatically awaited unless prefixed with background, I'm not sure about the value of having a "clear indicator" of which function calls are sync and which are async - in normal use I wouldn't care. I just want their results and if I want to make a function async (by adding a server call, etc.), I can do so transparently without breaking my code. I agree with the thrust of this proposal.

The horse has bolted on await async, but maybe with different keywords, e.g. asyncauto and background, I would definitely use this new syntax instead of await async (which I use currently), for the reasons stated in the original post. Bug minimization is a big factor in the features I use, and should be THE driving factor in introducing new features.

The OP has stated this idea upon extensive real world use, and I appreciate that. I would definitely support the introduction of e.g. asyncauto and background, and would definitely use it immediately in all cases instead of async await if introduced.

# Steven Mascaro (10 months ago)

Thanks Naveen, I agree, potentially in full.

Let me address T.J. Crowder's objections out of order (2,3,1), as the response will be clearer that way.

  1. As Naveen noted, this is a use case that I did (and very much intended to) handle in the proposal by having a keyword at the call site. Namely: let promise = async remote.runInBackground();.

  2. I think the principle "prefer explicit to implicit" argues in favour of the proposal (and against the existing syntax). You gave the example:

let x = foo();
let y = bar();

Imperative language conventions imply the second statement only executes after the first is fully complete. But if foo and bar are defined as async functions, the second statement likely starts executing before the real work being done in the first statement is complete. The proposed syntax restores the normal imperative conventions, and requires one to be explicit when code departs from those conventions. e.g.:

let p = async baz(); // We're explicit that this doesn't work like every
                     //other sync function, it just returns a promise!
let x = foo(); // This could be an await function or an ordinary sync
               // function. We don't need to care (outside of
               // atomics,etc.)
let y = bar(x); // Ditto

If you're really committed to being explicit, then we should require the coder to specify how they want every call to an async function to be handled. e.g.:

let p = async baz(); // Explicit
let x = await foo(); // Explicit
let y = await bar(x); // Explicit

But this seems needlessly verbose.

  1. I'm not committed to keeping the async and await keywords. My original thoughts on this were much like Naveen's proposed keywords. I was initially using awaitalways for await in the function declaration and trap for async at the call site, but if we do want to use different keywords, I think I prefer awaitauto and background. (awaitauto rather than asyncauto because I think it's very important to be clear about what the default call behaviour becomes.)

However, I later realised that the keyword reuse will only be confusing to someone who has only learned the existing (essentially incomplete) async/await syntax. Someone entirely new to the language may actually find the completed symmetry of the proposal less confusing. The explanation for a new JS developer might run as follows:


Asynchronous Functions

An "asynchronous function" is any function that returns a Promise.

async and await control how an asynchronous function is executed. Prefixing a call to an asynchronous function with async will cause it to be run in an asynchronous thread, and will return the Promise. Prefixing a function call with await will cause execution of the current function to be suspended until the asynchronous function has completed (successfully or otherwise), potentially returning a value computed by the function. Specifying how asynchronous functions should be called every time is cumbersome and error-prone, so you can specify the default approach to calling the function in the function declaration. Thus:

async function foo() {}

specifies that all calls to the function foo will use the async method by default and

await function bar() {}

specifies that all calls to the function bar will use the await method by default. In either case, you can override the default and force the call behaviour by prefixing the call with either await or async.


And I think this possibly makes things far clearer than any description I've yet seen, not because it was especially eloquent, but simply because the missing pieces have now been filled in.

Nonetheless, I could be convinced otherwise, and I'd be more than willing to accept Naveen's suggestions for different keywords.

# Steven Mascaro (10 months ago)

Oh, and just to be clear, there would still need to be an explanation for why you need to attach async or await to any function that contains a call to an asynchronous function. I assume that requirement exists for performance reasons --- I have suggested that requirement be dropped elsewhere, but understand that may not be practical.

# T.J. Crowder (10 months ago)

On Mon, Dec 4, 2017 at 12:52 PM, Steven Mascaro <subs at voracity.org> wrote:

  1. I think the principle "prefer explicit to implicit" argues in favour of the proposal (and against the existing syntax). You gave the example:
let x = foo();
let y = bar();

Imperative language conventions imply the second statement only executes after the first is fully complete. But if foo and bar are defined as async functions, the second statement likely starts executing before the real work being done in the first statement is complete.

I understand what you're saying there, I just disagree. I said I know the temporal sequence when looking at that code, and that's what I'm saying we should continue to be explicit about when it isn't "this then that" without the job yielding and potentially another job being scheduled in the meantime. That kind of yield for other work is a big deal and should, in my view, be flagged. I get the desire for it to represent the logical sequence instead without flags, I just think it's too late to do that to JavaScript. I keep flirting with the idea of a purely-async or async-is-the-assumption language and this is part of why, but I don't see retrofitting JavaScript to be that language. Perhaps that's pessimistic of me, but when these two functions have completely different temporal semantics, I think it's time for a new language:

(await () => {
    foo();
    bar();
})();
(() => {
    foo();
    bar();
})();

Maybe before the current async/await were added, but...

If you're really committed to being explicit, then we should require the coder to specify how they want every call to an async function to be handled.

Clearly not, that's just reductio ad absurdum. We have a massively well-established "If it's a plain call, it happens in the same job in linear sequence" precedent. For me (and we just disagree on this, which is fine), if it's going to be a yield back to the queue, it needs signifying.

Those are my $0.02, FWIW. Maybe I'm being old-fashioned. :-)

-- T.J. Crowder

# T.J. Crowder (10 months ago)

Apologies for the double-post. I meant to end with:

All of that said, you are talking about opt-in behavior. And that makes it an easier sell in many ways. Doesn't make it less confusing having two competing async syntaxes, and I think that's a major issue, but with the opt-in at least I know (if I'm paying attention) that when I'm looking at what seems like a sequence of calls in temporal sequence, I know it isn't, it's logical sequence instead. :-)

-- T.J. Crowder

# Bob Myers (10 months ago)

It might be useful to consider the ControlFlow notion used in writing selenium tests. All the steps in a test routine, which appear synchronous, actually implicitly introduce the equivalent of promise/then structures. They took this approach because almost all such integration tests are asynchronous at their core, and they did not want to force people to write huge sequences of then calls.

It turns out that this approach has a number of problems. As a result, future versions of selenium will no longer support it. Test writers will be asked to write await where needed.

For more information on this, see seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/promise.html, SeleniumHQ/selenium#2969, www.assertselenium.com/selenium/promise-manager-in-webdriverjs

-- Bob

# T.J. Crowder (10 months ago)

On Mon, Dec 4, 2017 at 2:37 PM, Bob Myers <rtm at gol.com> wrote:

It turns out that this approach has a number of problems. As a result, future versions of selenium will no longer support it. Test writers will be asked to write await where needed.

Were there problems other than the complexity of maintaining the promise manager tool and issues with debuggability? Neither of those seems like it would be an issue with Steven's proposal...

(I just realized for the first time, Bob, that your initials are RTM. I love it. You should adopt Frank as a second middle name. ;-) )

-- T.J. Crowder

# Isiah Meadows (10 months ago)

Am I misunderstanding something about this proposal that it's substantially any different from .then or immediately invoked async functions?

// Original
await function foo() {
    const bar = async baz()
    use(bar)
}

// What I'm reading
function foo() {
    ;(async () => {
        const bar = await baz()
        use(bar)
    })()
}
function foo() {
    try {
        Promise.resolve(baz())
        .then(bar => { use(bar) })
    } catch (e) {
        Promise.reject(e)
    }
}
# Naveen Chawla (10 months ago)

Obviously. The whole point of this proposal is that awaiting async functions is automatically implied inside an autoasync function, unless an async function is called with a background qualifier (which thereby makes it return its promise instead). The OP is right: this is a far less bug prone way to do async programming than async await, while offering all its functionality, with the added bonus of transparently allowing sync functions to be converted to async functions without fundamentally affecting consumer calling code.

For those who want to be able to do extensive async programming, having this in the language, and using it instead of await async throughout, is a no-brainer.

Of course, I am qualifying that it must be new keywords, not await async juggled like in the original post, but that wasn't the thrust of the proposal anyway.

# Naveen Chawla (10 months ago)

Steven, this is a replacement for await async, and makes await async redundant. There is no point teaching await async if you can do asyncauto and background, just like there's no point teaching var if you can do let and const. It should be a clean break. Otherwise it introduces a different kind of bug: not remembering which way around the keywords go.

Why I prefer asyncauto instead of awaitauto is because it still has to be made clear that it's an async function. It is awkward to have awaitauto function instead of asyncauto function.

# Alexander Jones (10 months ago)

with the added bonus of transparently allowing sync functions to be

converted to async functions without fundamentally affecting consumer calling code.

It does fundamentally affect calling code if the calling code reaches outside of its local variables. The state of the world around you might change any time you await and give control back to the event loop.

# kai zhu (10 months ago)

you can avoid this entire debate by using trusty and simple callbacks instead. here's real-world example of using a callback like you would async/await, to asynchronously test various states/scenarios for user-login in an app (using recursion to avoid callback-hell):

kaizhu256/node-swgg/blob/2017.7.24/test.js#L918, kaizhu256/node-swgg/blob/2017.7.24/test.js#L918

local.testCase_userLoginXxx_default = function (options, onError) {
/*
 * this function will test userLoginXxx's default handling-behavior
 */
    var modeNext, onNext;
    modeNext = 0;
    onNext = function (error, data) {
        modeNext += 1;
        switch (modeNext) {
        case 1:
            // cleanup userJwtEncrypted
            delete local.userJwtEncrypted;
            // test userLogout's default handling-behavior
            options = {};
            local.userLogout(options, onNext);
            break;
        case 2:
            // validate error occurred
            local.assert(error, error);
            // test userLoginByPassword's 401 handling-behavior
            options = { password: 'undefined', username: 'undefined' };
            local.userLoginByPassword(options, onNext);
            break;
        case 3:
            // validate error occurred
            local.assert(error, error);
            // validate statusCode
            local.assertJsonEqual(data.statusCode, 401);
            // validate userJwtEncrypted does not exist
            local.assert(!local.userJwtEncrypted, local.userJwtEncrypted);
            // test userLogout's 401 handling-behavior
            options = {};
            local.userLogout(options, onNext);
            break;
        case 4:
            // validate error occurred
            local.assert(error, error);
            // validate statusCode
            local.assertJsonEqual(data.statusCode, 401);
            // validate userJwtEncrypted does not exist
            local.assert(!local.userJwtEncrypted, local.userJwtEncrypted);
            // test userLoginByPassword's 200 handling-behavior
            options = { password: 'secret', username: 'admin' };
            local.userLoginByPassword(options, onNext);
            break;
        case 5:
            // validate no error occurred
            local.assert(!error, error);
            // validate statusCode
            local.assertJsonEqual(data.statusCode, 200);
            // validate userJwtEncrypted exists
            local.assert(local.userJwtEncrypted, local.userJwtEncrypted);
            // test persistent-session handling-behavior
            local.apiDict['x-test crudNullGet']._ajax({}, onNext);
            break;
        case 6:
            // validate no error occurred
            local.assert(!error, error);
            // validate statusCode
            local.assertJsonEqual(data.statusCode, 200);
            // validate userJwtEncrypted exists
            local.assert(local.userJwtEncrypted, local.userJwtEncrypted);
            // test userLogout's 200 handling-behavior
            // test jwtEncoded's update handling-behavior
            options = { jwtEncrypted: local.jwtA256GcmEncrypt({ sub: 'admin' }) };
            local.userLogout(options, onNext);
            break;
        case 7:
            // validate no error occurred
            local.assert(!error, error);
            // validate statusCode
            local.assertJsonEqual(data.statusCode, 200);
            // validate userJwtEncrypted exists
            local.assert(local.userJwtEncrypted, local.userJwtEncrypted);
            // test userLogout's 401 handling-behavior
            options = {};
            local.userLogout(options, onNext);
            break;
        case 8:
            // validate error occurred
            local.assert(error, error);
            // validate statusCode
            local.assertJsonEqual(data.statusCode, 401);
            // test userLoginByPassword's 400 handling-behavior
            local.ajax({ url: '/api/v0/user/userLoginByPassword?password=1' }, onNext);
            break;
        case 9:
            // validate error occurred
            local.assert(error, error);
            // validate statusCode
            local.assertJsonEqual(data.statusCode, 400);
            // test userLogout's invalid-username handling-behavior
            options = { jwtEncrypted: local.jwtA256GcmEncrypt({ sub: 'undefined' }) };
            local.userLogout(options, onNext);
            break;
        case 10:
            // validate error occurred
            local.assert(error, error);
            // validate statusCode
            local.assertJsonEqual(data.statusCode, 401);
            onError(null, data);
            break;
        }
    };
    onNext();
};

and here’s screenshot of setting a single-breakpoint to easily debug and step through all 10 asynchronous switch-statements in the callback. you can reproduce the test-case in screenshot with this link: kaizhu256.github.io/node-swgg/build..beta..travis-ci.org/app/?modeTest=1&modeTestCase=testCase_userLoginXxx_default, kaizhu256.github.io/node-swgg/build..beta..travis-ci.org/app/?modeTest=1&modeTestCase=testCase_userLoginXxx_default

# Naveen Chawla (10 months ago)

kai, no, that gets less and less manageable as the complexity of the async flow increases

# Isiah Meadows (10 months ago)

Kai, please read this article. Also, please stop filling your emails with walls of code - it makes it much easier to comprehend. If you feel the need to post pictures, please find an image hosting service or something. If you feel the need to show large amounts of code, please try using GH Gists, Pastebin, or similar. It just gets in the way of reading the emails, and not everyone can even see the images (notably if they aren't using an HTML-supporting client).


Isiah Meadows me at isiahmeadows.com

Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com

# kai zhu (10 months ago)

@isiah thx for link and read the article (i actually enjoy reading most of the links you post on this mailing list).

we can agree to disagree - i've been using recursive-callbacks for 5 years now. they’re quite powerful and elegantly solve the callback-hell issue. recursive-callbacks can do pretty much anything you can do with async-await / generators / promises, are very easy to debug with breakpoints, and most importantly have no unobservable magic going on behind-the-scene.

# Florian Bösch (10 months ago)

as I predicted once you use it, await/async infests all calls/funcdefs, and has now become pointless line-noise that you need to remember to write or you will get into trouble, but serves no discernable semantic, syntactic or logical function other than making you type more.

solution: write a transpiler that inserts await/async into all calls/funcdefs

# Isiah Meadows (10 months ago)

The real solution to that would be stackful coroutines, but those are tricky to implement and are substantially slower at lower nesting levels. (Go's goroutines are built from this under the hood.) There's tradeoffs to be made.

# kai zhu (10 months ago)

On Dec 7, 2017, at 5:50 PM, Isiah Meadows <isiahmeadows at gmail.com> wrote:

The real solution to that would be stackful coroutines, but those are tricky to implement and are substantially slower at lower nesting levels. (Go's goroutines are built from this under the hood.) There's tradeoffs to be made.

that is never going to happen. unless the goal is to intentionally sabotage javascript’s fundamental-design and cause even greater chaos and instability in the world of frontend-development.

# Naveen Chawla (10 months ago)

How has using async await made you type more? Can you give an example? I suspect you're not using it in the way it was intended to be

# Florian Bösch (10 months ago)

On Thu, Dec 7, 2017 at 2:34 PM, Naveen Chawla <naveen.chwl at gmail.com> wrote:

How has using async await made you type more? Can you give an example? I suspect you're not using it in the way it was intended to be

See example OP pasted. It's nothing but async/await. It doesn't add any semantic, syntactic or logic thing to the code. It could be 4 white-spaces and you'd not loose any meaning.

# Naveen Chawla (10 months ago)

You've lost me. It's not intended to add logic. It's a replacement for callbacks, and makes expressing async data flows simpler & more manageable, allowing more complex async data flows to be expressed more quickly and be less prone to bugs. The autoasync background concept makes this even more so. Retaining all the functionality, increasing rate of productivity. That's the whole point.

# Florian Bösch (10 months ago)

I fail to see the increased productiveness by converting:

FunctionExpression => function FunctionSignature Block

To:

FunctionExpression => async function FunctionSignature Block

And the equivalent for the grammar for await. Probably:

UnaryExpression => await RealUnaryExpression

Or something

# Naveen Chawla (10 months ago)

Um, no the increased rate of productivity is in converting

getUserInfoAsync()
    .then(
        userInfo=>{
             //Do stuff with userInfo
        }
    )

into

const userInfo = await getUserInfoAsync();
//Do stuff with userInfo

...allowing complex async data flows to be expressed much more simply, hence more quickly, more manageably and with less chance of bugs/mistakes. The autoasync background concept makes this even more so, hence why I would use it throughout instead of await async if introduced

# Алексей (8 months ago)

I think there is something we could have right now to solve the problem of missing await without changes to the ES - it should be collored differently in IDE or texteditor you are using. Not like an error - because it's actually not. But to be obvious that "here" and "here" you have an async function calls and values are promises

2017-12-08 6:27 GMT+02:00 Naveen Chawla <naveen.chwl at gmail.com>:

# Florian Bösch (8 months ago)

On Mon, Feb 12, 2018 at 11:27 AM, Алексей <agat00 at gmail.com> wrote:

I think there is something we could have right now to solve the problem of missing await without changes to the ES - it should be collored differently in IDE or texteditor you are using. Not like an error - because it's actually not. But to be obvious that "here" and "here" you have an async function calls and values are promises

You could also have the IDE automatically insert await automatically whenever it a codeline calls out to an async function. To make it more convenient, the IDE could then also automatically insert an async/await into any intermediary call-stack. You could also detect whenever a built-in async function is called, and do the same, so you don't need to designate it extra. Then make the async/await invisible and just color it.

Or, you know, just implement greenlets.

# Alexander Jones (8 months ago)

FTR, TypeScript and Flow (I assume) know when the static type of a function call (or any other expression that evaluates to a Promise) is a Promise, and if you try to use it as if it wasn't, it will almost surely be a type error.