I avoid async/await in JavaScript (2022)
uniqname.medium.com
uniqname.medium.com
I also don't like how async/await ends up taking over a whole codebase. Often making one function async will cause you to update other functions to be async so you can do proper awaiting. We literally exchanged callback hell to async hell, and said it was better, but I'm not strictly convinced of this.
Callbacks still have the function coloring problem: If you write a function which calls another function with an on-complete callback argument, then your function (usually) has to also take an on-complete callback in order to represent completion.
async/await makes this nicer, but it doesn't eliminate the underlying need to thread async completion all the way up your call stack.
This is basically the function coloring problem. There was some ardent discussion about this a few weeks ago on HN [1], where the top comment positioned function coloring as a feature, because it highlights where expensive operations like IO might potentially be happening in your application.
I saw a comment recently where someone pointed out an old API in Java where comparing two URLs for equality initiates a DNS request, and so something as simple as putting a URL in a hash table could end up involving a network request [2]. So maybe it's reasonable to color functions that connect to the network at the very least. If I used a URL comparison library and it returned a promise it would immediately set off alarms in my mind.
It's possible, but I can't thoroughly evaluate coeffects until they land in a mainstream language with production usage. What I do know from experience is that you can't quietly block the entire thread on a network request in some innocuous looking function in JS. You have to return a promise and then the calling function has the option to await that promise, but I'm sure the academics in their ivory towers have theorized about something better.
If there was a way for users to define their own coeffects then you could sidestep this issue, but I saw this quote in one of the design documents: "Runtime will have a native knowledge of each co-effect. We will not allow co-effects to be defined in user-land code [...] An important aspect to note here is that certain co-effects that need deep support from runtime such as io and pure will need to be implemented in the runtime" [2].
[1] https://docs.hhvm.com/hack/contexts-and-capabilities/availab...
[2] https://github.com/facebook/hhvm/blob/master/hphp/hack/doc/H...
It's obviously better. It's a bit unfortunate how much the history of the design decision shines through in the current solution, but it's unequivocally an improvement.
I would prefer an approach where calls to "async" functions are implicitly awaited unless a keyword turns then into a promise, and all functions are implictly treated as async as needed, unless a keyword specifies that they return a promise, which should be awaited instead. This would make the majority case clearer, and force you to make the minority case explicit where it's currently implicit.
I don't think this would help your coworkers who don't understand promises very much though.
> I would prefer an approach where calls to "async" functions are implicitly awaited unless a keyword turns then into a promise, and all functions are implictly treated as async as needed, unless a keyword specifies that they return a promise, which should be awaited instead. This would make the majority case clearer, and force you to make the minority case explicit where it's currently implicit.
This is a very interesting idea and feels good in an initial 'gut check' sense.
Both have their pros and cons. I've seen problems in Go codebases where some I/O operation blocks the main thread because it's not obvious through the stack that something _should_ best be run concurrently and it's easy to ignore until it gets worse (at which point it's annoying to debug).
const myAsyncThing = async (init) => {
let value = await task1(init);
value = await task2(value);
return value;
}
const myPromiseThing = (init) => task1(init).then(task2)
I know this is a relatively simple and contrived example, but there are some times where async/await very much is bulky and gets in the way of just writing code.Compare with
const myPromise = new Promise((resolve, reject) => resolve(”foo”));
Afterwards either try-catch await myPromise, or use myPromise.then().catch() const myAsyncThing = async (init) => await task2(await task1(init))
This makes the promise chain look much more like what it is, given that the rest of the language uses nesting calls to indicate "after".My argument here is that JavaScript is -- as well as most languages we use these days are -- fundamentally designed around the syntax of a function call, and so if you program in such for very long at all you get used to reading "inside out, right-to-left", as that's how function calls work. If we do not like inside out execution order, then we should fix the entire language (by using suffix notation... fwiw, I'd be totally on board ;P), rather than narrowly complain about it only in code which uses async/await and fixing it by abusing the OOP this argument exception to the ordering rule.
(I would also point out that the special syntax order exception you are relying on only works for a single argument, and so if you have to pass task2 both the result of task1 and a second argument to configure it, the usage of .then() breaks down unless we have a way to curry arguments, which we can always simulate using JavaScript's bind(), but at some point we are really going far out of our way to avoid coding using function calls, despite being in a language which relies on them everywhere else.)
At the end of the day, I am just looking for some consistency, so I can quickly and easily interpret what code does no matter whether it is synchronous or asynchronous and whether it takes zero, one, or more inputs.
const value1 = await task1(init);
const value2 = await task2(value1);I treat await as a code smell and always triggers me to evaluate if we really need to be waiting for the result in this place. Most of the time they shoukd just return the promise or put something in a then.
People are going to misuse stuff but it doesn’t mean we shouldn’t use appropriate tools when appropriate.
EDIT: this is especially true for NodeJS where lots of frameworks are promise/async aware and support you returning a promise to them.
The number of await calls in your codebase should always be greater than the number of async modifiers.
Instead of using await you can instead just use “.then(() => part that needs to wait)”, or a chain of “.then” on the returned promise to sequence dependent logic. Or if needing to wait on a set of async actions you have “Promise.all”. It is much more explicit on what part actually needs to wait instead of “everything after this in the function block”.
I prefer to just compose promises which allows you to sequence actions that need it. It is a code style preference but I find I end up with clearer codebases doing it this way.
Yes. Without using await, it's basically the same thing as just changing `return result` to `return Promise.resolve(result)`.
> Useful for wrapping logic that otherwise would be synchronous.
The logic is still synchronous, except now the return value is just wrapped in a promise.
> Instead of using await you can instead just use “.then(() => part that needs to wait)”
I don't understand why this would ever be desirable versus just calling the function synchronously. If for some reason you really need `.then` I would just wrap the call to the function, instead of making the fn async, `Promise.resolve(myFn()).then(() => part that needs to wait)`
> if needing to wait on a set of async actions you have “Promise.all”.
You can pass non-promise values to `Promise.all`, and it will work fine, although I don't see why you would do it intentionally. ie `await Promise.all([1, Promise.resolve(2)])// resolves to [1,2]`.
> It is much more explicit on what part actually needs to wait
If anything it's less explicit, you are disguising the true behavior. You making a synchronous function look like it's async when it isn't.
It's also going to suffer a small performance penalty, although it shouldn't really matter unless the function is used in some tight loop. When I tested calling a very simple `return 1` function, making it async resulted in the benchmark taking twice as long.
This is actually how rust handles async/await. If nothing is polling the future then the future is not running. If you want background execution like javascript then you need to hand it off to an executor (for example via `tokio::spawn`). Its actually pretty nice because then you can build a future and pass it around but you can decide if/when you want to execute it later.
That being said, you still wouldn't want to do a for loop awaiting each future.
I have a degree in Electrical Engineering, and don't understand how programmers can function not understanding physical computer architecture.
But clearly, that's not the bar which most come from.
It's a basic way to learn. In Python you don't directly teach people about decorators without first asking them to explicitly reassign a function to the result of calling the decorator. In Haskell you don't teach people about do notation without first making them write nomadic code with the bind operator and many lambdas.
In many cases, just a few days of forbidding students to use the syntactic sugar is enough to make them appreciate why this syntactic sugar is needed and how it is supposed to be used. JavaScript's async/await is the same.
I suggest that this is largely determined by things like the person's previous experiences, maybe even their group or personal identity, etc. and various factors that are not necessarily objective.
For me, I strongly feel that using async/await consistently instead of a promise chain style can clean up a codebase quite a lot.
I also feel that whitespace significant syntaxes are (to me) obviously cleaner. So for a year or two at least I used things like CoffeeScript or even ToffeeScript.
But I got tired of feeling like I was swimming upstream.
I also did a solo project with significant usage of LiveScript a few years ago. Which I think is a very nice language.
concurrently {
const user = await getUser(userId);
const session = await getSession(sessionId);
}
meaning that- the getUser and getSession calls occur concurrently (it desugars to Promise.all)
- user and session are available for use after the block
- user and session appear directly next to the calls that give them their values, unlike with Promise.all
async function concurrently(obj) {
const kvlist = Object.entries(obj);
const resolvedValues = await Promise.all(
kvlist.map(([_key, promise]) => promise)
);
return Object.fromEntries(
kvlist.map(([key, _v], index) => [key, resolvedValues[index]])
);
}
// Usage
// This could also be const {user, session} = ...
const info = await concurrently({
user: Promise.resolve(3), // or just 3
session: new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
}),
});
// info = {user: 3, session: 'foo'}
You're left with having to get at your results via dot syntax, or having to duplicate the object keys to destructure them, but you'd have to do that with Promise.all() anyway. This lets you make the definitions declaratively. try {
concurrently {
const user = await getUser(userId);
const session = await getSession(sessionId);
}
} catch (err) {
…
}try {
const [user, session] = await Promise.all([
getUser(userId),
getSession(sessionId)
])
} catch (err) {
…
}The author sees this code and realizes it's unnecessarily serial:
save('userData', userData)
.then(() => save('session', sessionPrefences))
.then(() => ({ userData, sessionPrefences })
But doesn't see that the async/await version is serial, even though it's arguably even _more_ obvious? await save('userData', userData);
await save('session', sessionPrefences);
return { userData, sessionPrefences }
The transform to being parallel is pretty easy to read too: await Promise.all([
save('userData', userData),
save('session', sessionPrefences)
]);
return { userData, sessionPrefences }
On top of that, the try/catch section is out of date. try/catch does not deoptimize the block in modern engines, and async/await absolutely integrates better with try/catch for much more bulletproof error handling.I tend to agree with this sentiment. I wonder whether it would have been helpful, perhaps in an alternate universe, for all functions to return a structure containing two things: the actual intended return value, and an error object (perhaps NULL if no error, to reduce overhead).
So something like `return $foo;` would imply no error but `return NULL, Error('bar')` would imply an error. Then, we could use `return` at any point to indicate success or an error depending on the case.
The only thing remaining to mimic the behavior of `try...catch` would be to make closures which could be `return`ed from, similar in concept to `throw` in that they would exit the block except that because it's a `return` it would work like any other `return` to exit the block.
Am I crazy, or would this (at least in theory) simplify the return/try/catch issues the author is talking about?
Yep, JavaScript is excellent as a multi-paradigm language. I pretty much use it as a functional one. I hear the JavaScript edition of SICP [1] works better than the Python one did.
I don’t know how in the world this guy thinks that async is hard to understand…
I mean I think it’s pretty obvious it’s an async call!
But I didn’t read the whole article!