My biggest gripe with async await is when it's used by people that don't understand promises. They interpret await as "let promise run" and not "this blocks the flow." I had someone show me code where they wanted to preload a bunch of images, and effectively had a for loop and did an await on each image in it, effectively running it as slow as possible. I showed him `await Promise.all(imageLoadingPromises)`, and he was confused why we needed `Promise.all` here.
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.
It's not async/await that takes over a codebase, it's the async nature of functions period, no matter if you're using async/await, Promises, or callbacks.
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.
> 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.
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.
That could be much better solved through coeffects, but unfortunately most languages don’t support that. Doesn’t mean using async functions is a good way for this either
> That could be much better solved through coeffects
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.
This looks pretty cool. One issue I have from just a cursory glance is that the IO capability includes things like `print` and `echo` [1]. If the context system can't differentiate between `console.log` and `fetch`, then it's a little less useful for me.
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].
Hack is custom made for facebook and its use cases, and there’s no use for distinguishing those kind of IO at Facebook. The main use case for the effects system is actually privacy, wherein where processes that aren’t privacy controlled cannot access user data or data that got processed by other functions that did. And that’s enforced by the runtime. It’s super cool and a great example of what those systems are capable of.
>We literally exchanged callback hell to async hell, and said it was better, but I'm not strictly convinced of this.
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.
That's basically how goroutines work in Go. You opt into concurrency with the `go` keyword, while it's blocking by default. While in JS it's concurrent by default and you opt-in to blocking with the `await` keyword. (Except in Go you have true parallelism for CPU-bound tasks too, while in JS it's only for I/O)
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).
I prefer the raw promise syntax. It makes the promise chain look more like what it is, an asynchronous pipeline, and you don't need to litter async everywhere.
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.
I didn't write it like that because it obscures the call order. I wasn't trying to compare code line/count, just flow control and readability, which is certainly subjective.
If this "obscures the call order" then isn't that a problem with the programming language in general? If these functions weren't async, would you also not be willing to use g(f()), as that "obscures the call order"? (I certainly have met people who do.) If we aren't going to use function calls, and we also do not want to put these functions on separate lines as that is "bulky", then are we going to argue that we should have something similar to .then() for synchronous code?...
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.
I love async but very rarely use await. It makes inversion of control patterns super easy and avoids deep logic and instead get something much more readable.
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.
Async implicitly wraps the return in a promise if it otherwise wouldn’t return one. Useful for wrapping logic that otherwise would be synchronous. So, syntactic sugar so you don’t have to have “new Promise” to wrap what would otherwise be synchronous logic.
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 agree. People should gradually build up their intuition from lower abstractions to higher. People should be banned from using async/await unless they had first experienced callback-based code, and then promise-based code. Going straight to async code is like asking people to run before they have learned to walk.
They need to have experience and intuition working with promises without syntactic sugar, before being allowed to use syntactic sugar.
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.
It's interesting to me how many people can look at the exact same thing and have completely different experiences and takeaways.
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.
This doesn't seem too awful to do with a function instead, and doesn't require any new language syntax or features.
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.
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.
> Now, there are some schools of programming that lean heavily into try/catches. Me, I find them mentally taxing. Whenever there is a try/catch, we now have to worry not only about what the function returns, but what it throws. We not only have branching logic, which increases complexity but also have to worry about dealing with two different paradigms. A function may return a value, or it may throw. Throwing bubbles if it is not caught. Returns do not. So both have to be dealt with for every function. It’s exhausting.
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?
> It’s just a tiny hint to get you thinking about what functionally style JavaScript could look like if we wanted.
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 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.