Beware of Async/Await
brandons.me
brandons.me
Next up: exception handling doesn't work if you never catch exceptions! Followed by, assigning a pointer to another variable doesn't assign the value the pointer points to! :P
For me I still had my mental model from a lazy execution environment (Dask) and that’s where my misconception came from.
That said, I agree with the GP. I immediately saw that it was going to execute sequentially and thought nothing of it. Most of the time I use async/await not for concurrency but for syntax sugar and to get proper exceptions from promise based APIs instead of the GOTO FAIL control flow breaking nonsense that is catch().
const getPeople = async () => (await Promise.all(...)).flat()
Without await this would be
const getPeople = () => Promise.all(...).then(arrs => arrs.flat())
The difference is slight to be sure, but I usually prefer the await approach.
Even Wikipedia says the async/await pattern "allows an asynchronous, non-blocking function to be structured in a way similar to an ordinary synchronous function."
Honestly not sure how this made it to even 20 points.
It's also behaves like synchronous I/O, which is the whole point of the notation.
Which isn't to say all is rosy in async/await world. Debugging them (in JS in particular) is particularly painful.
async/await is clearer to me after reading this article though.
This is a fairly common mistake, but it’s also easy to spot.
async function getPeople() {
const [members, nonMembers] = await Promise.all([
fetch("/members"),
fetch("/non-members"),
]);
return members.concat(nonMembers);
}
The gist of the article is very true though. This is something that comes up every now and then in code review, even with seasoned developers. I'm not entirely sure what the solution would be since depending on the context, both sequential and parallel execution can be correct. It might just be one of those rite of passage type of mistakes you have to make once or twice. async function getPeople() {
const membersPromise=fetch("/members");
const nonmembersPromise=fetch("/non-members");
return [].concat(await membersPromise, await nonmembersPromise);
}
edited to fix typo spotted by lhorie. thanks.Your question about Rust futures makes a ton more sense now. This only works because a promise is self-executing / not inert like a Rust Future. Thank you kind stranger! Your async-foo is strong ^_^
IIUC, work to be done for a promise is (typically) placed in the task queue when the promise is created, and will be eligible to start running the next time you get back to the event loop, which is part of what an `await` does.
The asynchronous portion of the work is enqueued when the function is called, but does not start executing until it's scheduled, which can't happen until the current task ends or is blocked by an await. This is useful, as it means a promise can't settle before you've had a chance to add handlers; otherwise you might sometimes get UnhandledPromiseRejections because of an unavoidable race condition.
I said arguably correct because there's nothing stopping the function from doing a portion of the work synchronously, which would mean the work as a whole starts with the function call.
const getPeople = async () => (await Promise.all(...)).flat()
async function getPeople() {
return fetch('/people')
}
just seems far more straightforward across the entire stack (where each entry has a `member: boolean` or `type: someEnum` field)And their discussion of Promises: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promis...
However, after reading the article it is, at worst, a performance problem.
...and performance should come after the code is correct and I don't think the code is even semantically correct in the first place.
I'm assuming that "members" and "non-members" are distinct subsets of all People. i.e. it's not possible to be in the "members" and "non-members" sets at the same time. Additionally, everyone is in one or the other.
Because the set membership is queried with 2 independent queries (designed in a reasonable and common RESTful manner) there is a race condition where an object (that changes in the time between the two calls) might appear in neither or both of the sets.
These results are then blindly concatenated together.
If the API wants to retain its RESTful design, it must include metadata about the consistency between calls. For example some kind of token that can be compared to check that the replies to the two queries represent a consistent view of the data.
If the tokens, differ, the "transaction" can be retried.
Alternatively, the API can be designed, possibly making it less RESTful in the process, so that the transaction is implemented on the server.
There are many other ways to guarantee a correct and consistent result.
Either change has big enough implications that optimising for performance at this stage would be premature because of the amount of refactoring necessary to implement `getPeople` in a way that provides consistent and correct semantics.
fetch(x); fetch(y);
vs
await fetch(x); await fetch(y);
async function getPeople() {
const members = fetch("/members");
const nonMembers = fetch("/non-members");
return (await members).concat(await nonMembers);
}So no, performance does matter a lot here.
FWIW I worked in the search/database space for a decade and it's the same thing there. There was one time I wrote a lock-free hash table implementation for a registry that needed to be optimized for concurrent access; that code was much harder to write and to understand than the rest of the codebase, but it was justified. Most of the codebase, though, was not performance-critical.
EDIT: I will grant that you still need to think about how to architect the project so that it is optimizable later, which can mean thinking about what needs to be async early on.
I'm just making a bog-standard argument against premature optimization, and I'm shocked that we can't find common ground.
On the server, maybe having a set of requests take two seconds instead of one second (without consuming extra CPU cycles) doesn't really matter. On the front-end, it matters a whole lot. People go to much greater lengths to shave off much smaller amounts of time. Otherwise people on orange websites complain about your site being slow ;)
On the server, maybe it's a really common case to be making requests that modify state, and/or to be stitching together several microservices that require IDs or other information to be passed between them. On the front-end, especially on first-load, you're often loading up several different datasets purely for display purposes. These are rarely written to depend on each other, because of the latency issue in my previous point.
It is interesting though to see that there's a difference in norms/use-cases between the two.
The main difference is how much latency a typical request is likely to have.
On the backend, this optimization would frequently only save you 50 ms, because you are making a low latency call to another service that you are running in the same datacenter.
Edit: Sorry for the late edit, but: Is it because users are using it thinking it just "unwraps" a promise into its resulting value? I can begin to understand that, but it seems so incidental to the primary concept of "waiting" for something asynchronous to return.
The article says this is correct.
> const both = await Promise.all([ members, nonMembers ]); > return both[0].concat(both[1]);
I would have written it this way without the third promise.
> return (await members).concat(await nonMembers);
Is this wrong somehow?
The point of `Promise.all` is to have every promise passed to it to start computing concurrently, likely in parallel if they e.g. start independent I/O operations.
// Given:
const fooPromise = asyncFoo();
const barPromise = asyncBar();
// Then this...
await fooPromise;
await barPromise;
// ...is equivalent to this:
await Promise.all([fooPromise, barPromise]);
Now, Promise.all() is useful, but it is unrelated to when computation starts.The misunderstanding in the article has to do with misunderstanding when a promise is executed. It’s easy to wonder why calling await on a promise directly vs calling it on a variable containing a promise is different. The difference is promises execute immediately on creation while await blocks on a promise. So a promise that is created after an await will be executed after that awaited promise is resolved.
Consider:
const x = await foo();
await bar(x);
How can you schedule `foo()` and `bar()` in parallel, when they have an explicit data dependency?You can't, but that's a fundamentally different scenario than what's being discussed.
`await` should just return control to the event loop (ie. actually transform the remaining code into a callback). If you've issued the two promises before that happens both should be able to start operation.
As far as I know the difference here would be effectively (in very loose pseudo-code:
fetch(first).then {
fetch(second).then {
doThings();
};
};
vs. a = fetch(first);
b = fetch(second);
a.then { b.then { doStuff(); } };
which to my understanding should do both fetches in parallel even though the resolution is not.Looking at a bunch of articles about "how Promise.all can be implemented" it looks like this kind of recursive resolution of the promises is one of the methods that's expected to work (if not perform terribly well in the general case)?
See this SO answer for eg. as well: https://stackoverflow.com/a/46348155
I expected `await X` to create the promise `X` when it starts executing, much like `cond_A || cond_B` only computes `cond_B` when `cond_A` has been computed and turned out falsy.
Else how the following code ever work correctly?
const x = await foo();
const y = await bar(x);
We cannot schedule `bar()` before we have computed its inputs.OTOH `(await x()).concat(await y())` could speculatively compute both promises concurrently, even if it does not yet know whether the result of `x()` has a property named `concat`. I won't normally expect it, but JS is used to defy logic in many areas :-\
In the example you've shown, it is absolutely correct that the two promises must be sequentially awaited like that. The OP is talking about a situation where there is no dependency between x and y.
Personally I agree that this is cleaner than the Promise.all() solution, but I also think it emphasizes just how misleading the syntax can be, given that those weighing in on this thread can't even agree what this code will do without trying it out.
await fetch
await fetch
concat
and fetch
fetch
concat await
is that the fetch can actually perform asynchronous web activity in the latter example.> the first `await`, when executed, immediately pauses execution
Also true, but in my code, the first `await` happens after the second `fetch()` has already begun.
My understanding is that `await` refers to the resolution of a promise, not its start.
Sure. There's no error handling :-)
(Also, if it's not some magic fetch, the return value needs to be json-ed.)
Also, how should the function handle errors? It's not immediately obvious. An empty array is not the correct thing to return (an empty array could be a valid return value). Returning undefined/null would be another option, but that hides the error from the caller (which might be interested in the exact error message, to be able to properly deal with it).
try { ... } catch (e) { throw “an unknown error occurred” }
;)
"What's wrong with this code? It tries to make `async` look more readable by skipping the ugly try/catch block/s" ;)
async function getPeople() {
concurrent {
const members = await fetch("/members");
const nonMembers = await fetch("/non-members");
}
return members.concat(nonMembers);
}The reason is that it’s easy to make a mistake when destructuring the Promise.all call, and the Promise.all call doesn’t scale well (here dummy example, spot the bug)
const [foo, baz, bar] = Promise.all([getFoo(), getBar(), getBaz()])
This is much more readable and less error prone: concurrent {
const foo = await getFoo();
const bar = await getBar();
const baz = await getBaz();
}The hack approach otoh introduces an entirely new syntax construct (tagged blocks), which declares variables that escape their initialization block (unexpected in the JS world post-let/const), and strongly leverages the existing hack execution model whereby all await’s in a single statement are executed concurrently. This execution model is incompatible with the rest of JS (hack disallows many styles of having multiple awaits in an expression, such as `await a && await b`), and at the end of the day what are the benefits? It executes a series of statements in parallel, exactly the same as Promise.all.
If you’re concerned about mixing and matching declarations, maybe try
let foo, bar, baz;
await Promise.all([
foo = getFoo(),
bar = getBar(),
baz = getBaz()])
?Edit: realized in the above example the variables remain of promise type, which isn’t great. One could alternatively do “getFoo().then(result => foo = result)”, but I agree this isn’t great either.
Maybe someone could built this for typescript, the strict typesystem should make this easily (?) solvable by some hidden wrapper type, like AwaitedButUnaccesedPromise.
Running them in parallel is not a solution. I would create a new API call to fetch both.
What you would need is a single HTTP request that uses a DB read transaction to get the whole set of people.
It's amazing what gets upvoted on HN.