async/await -> await/async: a simpler, less error-prone async syntax
I understand the motivation, but I have to disagree with the proposal for multiple reasons:
-
It adds too much confusion to the existing syntax.
-
If the calling code needs to call one of these new-style
await
functions and access its promise (rather thanawait
ing 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 intoPromise.all
orPromise.race
). (Obviously you could add something to make it possible to access the promise.) -
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
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.
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.
-
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();
. -
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.
- I'm not committed to keeping the
async
andawait
keywords. My original thoughts on this were much like Naveen's proposed keywords. I was initially usingawaitalways
forawait
in the function declaration andtrap
forasync
at the call site, but if we do want to use different keywords, I think I preferawaitauto
andbackground
. (awaitauto
rather thanasyncauto
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.
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.
On Mon, Dec 4, 2017 at 12:52 PM, Steven Mascaro <subs at voracity.org> wrote:
- 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
andbar
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
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
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
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
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)
}
}
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.
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
.
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.
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
kai, no, that gets less and less manageable as the complexity of the async flow increases
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
@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.
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
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.
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.
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
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.
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.
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
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
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>:
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 anasync
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.
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.
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.