First off, async/await is promises. It's merely syntactic sugar. The point of async/await is not to never have the word "Promise" appear in your code. It's also not meant to be universally better than using Promises bare-bones.
A lot of the argument appears to be the author extrapolating from his own lack of familiarity to others: "We are taught", "our minds", etc. I can easily construe some hypothetical person with a certain set of skills (and lack thereof) that would have just as much trouble with bare-bones Promises. But I don't have to because the author did it for me at "One more thing…".
"People can mess this up" was never an argument. You can mess everything up. The more interesting question is how badly and how often.
Agreed, and I think some authors need to be more humble. If a construct or design choice exists, its far more likely someone (or a team) spent hundreds more hours intentionally reasoning for its existence in the first place; over your passive duration of usage.
async/await is _safer_ too, because part of that syntactic sugar is wiring up exceptions correctly and ensuring that a function only returns or rejects asynchronously and never synchronously. This prevents lots of bugs.
new Promise() is not how you want to create a promise in JS (except when you really have to), just like 'new'/'delete' are not how you ant to allocate memory in C++ (except when you really have to). There are lots of helpers in that class that make promises significantly more ergonomic.
You can use this in place of new Promise() (though you rarely need it, as an async function automatically wraps any non-promise return value in a promise.)
For making requests in parallel, and returning the first successful response.
--------
As mentioned in sibling comment, things like fetch and DB calls return promises anyway, so the above are mostly only useful for working with multiple other promises.
I've been on teams working on node projects for like 15 years. I've never seen someone who understands promises or async await do that. You'd have to know nothing about what the async keyword actually does to be compelled to do that.
> I've never seen someone who understands promises or async await do that. You'd have to know nothing about what the async keyword actually does to be compelled to do that.
I feel like there are advantages to making it `async function`, even if it's superfluous, because it signals to readers and to static code analysis that the function returns a promise. That's assuming the return type of fetch(...) can't be inferred by static analysis and developer tooling.
I'm a newbie with respect to JS and especially promises and async/await, but I need to learn. If you could point me to some resource that does a really good job of explaining all this, I'd appreciate it very much. I expect that I wouldn't be the only one. What's something you'd recommend to a junior developer so that they wouldn't be one of the "many devs", as you put it, who do the wrong thing?
Not the parent, but I recommend understanding the event loop first: here [0] is a very good talk. Then, read the chapter on promise on javascript.info [1], as it explains the problems Promise set out to solve (callback hell), then as usual the excellent MDN article [2].
The whole async/await paradigm (including promises) is trying to fit square pegs into round holes (trying to make async processes look like they are synchronous). Just look at the comments on this page; even though async/await/promises have been around for years they are still causing difficulties for even the most experienced devs.
It doesn't mean it can't be mastered, it can, but the whole paradigm is so fraught with pitfalls and conceptual difficulties that an codebase that uses it in any extensive way will forever be unstable. The whole thing is supposed to help against callback hell, but that can be better solved by a simple thenable object.
Actually no. A promise is a thenable oject, true, but a complex one. Simple thenable objects are in the simplest form objects that internally just holds an array of methods defined in each then() block. These methods then execute one after another at runtime. Any slightly experienced dev can probably create a library object or class like this in under 100 loc as a dropin replacement for most promises needs, and get a firm grasp of the internals in addition.
The usage of such an object won't be as terse and seemingly elegant as await syntax, but this is part of the problem: with await/promises so much of the complex logic is hidden from view and instead needs to reside the head of each dev, where it needs to compete with a thousand other things that need attention. It's an expression of the constant but unhealthy tendency towards golfing that pervades our field IMO.
Yeah, up until you need some extra functionality and you fall into the Inner-Platform effect (https://en.m.wikipedia.org/wiki/Inner-platform_effect). Promises are a well-established and battle-tested standard, I'd be very weary of someone reimplementing them for no reason.
This doesn't directly answer your question, but... something to watch out for is experienced developers can also struggle with promises and async code if they've spent most of their career working with sync code. And when we 'get' it, the difficulty of the journey is often understated. This stuff can be hard, so don't sweat it if it seems frustrating. (On the other hand, it may be easier if you don't have years of sync patterns to mentally set aside )
It really isn't, most native/standard APIs do not return Promises. Lots of things use callbacks. Being comfortable creating Promises from a callback-based API is definitely something any competent JS dev ought to be able to do.
A lot of APIs produce promises these days. The big one that I always need to promisify is `setTimeout`, but apart from that, I tend to find that if I'm using the `new Promise` API, I'm usually doing something wrong.
With Node APIs, there's a promisified version of pretty much everything. With browser APIs, there's usually a version with promises, and I'm struggling to think of an asynchronous API without promises that hasn't been superseded by something else (e.g. XMLHttpRequest -> fetch). If I'm converting from an event-emitter API to promises, there's usually going to be an impedance mismatch between the expectations of the event-based system and the promise-based system, and I probably need to explore another option.
I agree that any competent JS dev should understand how to create promises "from scratch" like this. But it still should probably be a fairly rare occurrence, and if I see a lot of `new Promise` calls in one place, it's pretty much always a sign that someone doesn't really understand how promises work.
const myfunc = () => new Promise(resolve => resolve())
Is there anyone that actually likes this structure? I find it really hard to reason about what a line like this does. What is the upper limit on double arrows in one line?
It’s the cleanest way to wrap a event-emitting library, for example. async/await is not applicable in such a case where resolve needs to be explicitly called in a callback.
It does, but only if you also add a redundant await. Which I still do, even in TypeScript, unless there’s a compelling performance reason not to. I disagree with the article overall, but I do agree that making asynchrony as explicit as possible is a good idea. Otherwise you end up with code like:
async function foo(bar) {
// ...
}
function nonObviousAsyncFn() {
// ...
return foo(quux);
}
Explicit return types would help, but most people don’t write them unless they’re forced by a linter.
Your gripe is that there are two arrows on one line? The second is an argument to the promise constructor. You should be able to reason about this line easily.
True. But these days it's rare to see a scope other than what you'd expect from the short hand version. Old school JS with plain prototypes and flexible 'this' scope is very powerful, but is too hard to understand and thus rare these days.
That’s one of the neat things about JS: even outside the prototypal inheritance chain, classes can be (partially) composed of generalized, reusable functions, a bit like traits. Just import a function and bind it in the constructor (call).
> First off, async/await is promises. It's merely syntactic sugar. The point of async/await is not to never have the word "Promise" appear in your code
Sounds like you didn't understand the point of the article. Unfortunately, you're arguing against something the author never said, the author never said that async/await weren't promises. You just got stuck on the fact that the author used the term "promise" for the non-async/await style of code. I understood what the author meant, anyway.
> "People can mess this up" was never an argument
Unfortunately, you're arguing against something the author never said here, also. The author is saying that non-async/await code is better code than async/await. Not that "people can mess this up" or something.
> Sounds like you didn't understand the point of the article. Unfortunately, you're arguing against something the author never said, the author never said that async/await weren't promises.
Well. I never said that I didn't understand the article. Unfortunately you are arguing against something I never said. Funny how that works.
> The author is saying that non-async/await code is better code than async/await.
And what does "better" mean to the author?
The entire first section is devoted to the author talking about criteria such as "brittle"-ness, "error-prone"-ness, and "footguns".
You seem to be getting stuck up on the fact that the author never used the exact same wording as I did.
> The point of async/await is not to never have the word "Promise" appear in your code.
Indeed it is not the point, and in that sense your criticism of the article is correct.
However, there are people who argue[1] that it is, in fact, a good thing to never have the word "Promise" appear in your code, that not having the freedom to execute whatever code you want after launching an asynchronous operation and instead having the predictability of always returning to the scheduler makes reasoning about asynchronous code much simpler. (For one thing, you can now reliably think—and have syntactic support for thinking—in terms of sequential coroutines with predictable yield points and known parents.)
In the context that Smith refers to (trio vs asyncio in Python, regarding which also see the first, more Python-specific but IMO better-argued post[2]) my experience actually bears that out. How well this works given the historical API baggage in JavaScript, I don’t know, but probably not very well.
Another curious thing is that the line of research JavaScript promises originate from[3] (Mark Miller co-wrote the spec proposal IIRC) does not use them as the user interface; instead, using the "eventual send"[4], you queue up method calls to objects (which can be promised results of other queued calls, and you may pass other promised objects as arguments).
I cannot clearly articulate the relation of this to Smith’s notion of “structured concurrency” (I wish I could), but in any case it contributes some more weight to the opinion that the ergonomics of Promises are indeed subpar. It might well be, though, that in the context of JavaScript you can’t actually enforce enough structure on the preexisting APIs to build a superior alternative.
I would even say that async/await is anti-promise, it takes the main functionality of promises, a caching layer for results and errors that allows you to add the code continuation later and elsewhere (which is a major footgun imo) and coerces the execution flow back to going on the next line and provided immediately at compile time which results in a cleaner flow but not as clean, stateless, efficient or functional as if you were to remove the promises completely. Having an additional caching layer and state machine around every asynchronous function call is quite inefficient.
The essence of async/await is not promises, it's the underlying javascript generator (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...) functionality combined with asynchronous code to stop and start the generator. It's the ability to pause and resume function execution based on asynchronous operations.
The promise functionality, the caching layer and state machine for results is basically sanitized away with async/await, it becomes dead-weight computation. The only benefit of promises in async/await code is being able to more easily interface with other promise laden code which you don't need once you have async/await and a library like https://www.npmjs.com/package/async for more complex cases.
Note that promises based async/await is also a mess of an implementation that breaks stack traces and needs to support tons of odd statement corner cases (basically anything that can return an object that could be a promise) whereas a continuation passing style async/await would be a much simpler implementation that would only apply to function calls and maintain stack traces. We would get that stack trace support automatically because of the great work of whoever implemented javascript generators which seem to already carry stack traces across paused/resumed functions (if you don't wrap in promises).
I don't agree with you fully. Async most certainly always returns a promise and as such can easily be combined with promise functions, like Promise.all. Your understanding of stack traces on promises is also old/uninformed. I work on an enterprise grade node app that was started back in node 0.11. Bluebird, which was what one used, offers good stack tracing and improved stack tracing has been available for async await for a few years now. Infact, I don't think I have had to go out of my way to get a decent stack trace in about a year or more now. There are certain paradigms you can adopt that keeps promise chains clean just like you would adopt with callbacks. I can't comment on if async await was implemented poorly as you are certainly more knowledgeable there with the RFC you shared.
Exactly. Async/await just an alternative way to represent a promise chain.
My feeling is that mixing the use of promise chains and async/await can make code hard to follow, so I ask colleages to not mix them in the same function.
Sure, async/await is syntactic sugar for a generator that chains promises into a promise chain for you. Not sure why you'd care. That is an implementation detail that never gets exposed to the user.
Now maybe it’s just my familiarity with Promises, but I look at the third example and I can quickly see an opportunity.
This entire article is built around the author's ignorance and could easily be summarised as "I avoid async/await syntax because I'm more familiar with promises". The author doesn't even appear to understand that async/await is syntactic sugar for promises.
I did not take me long to reach the same conclusion. The article can be summarized as "I am ignorant of the meaning of async/await, thus I don't use it". This is perhaps one incremental improvement from "I am ignorant of async/await, thus I use it poorly". But in the wrong direction.
How would you handle two asynchronous saves which can happen in parallel without using a Promise.all? Don’t think you can…and that’s pretty much the entire point of the article.
Async/await is useless unless you are willing to serialize your calls defeating the entire point of async code.
> How would you handle two asynchronous saves which can happen in parallel without using a Promise.all?
This question doesn't make sense. Async/await is just a nicer syntax for interacting with promises. So my answer to your "gotcha" question is just:
await Promise.all([..., ...])
There's nothing impure going on here. The majority of the time, async/await can make it much easier to see a code's control flow by getting rid of most of the Promise related cruft and callbacks.
I would call Promise.all a benefit here, as it makes it stand out where I'm doing something in parallel.
I think you’re trying to recreate the semantics of Promise.all without using Promise.all.
You’re effectively saying that Promises are a better async programming paradigm than async/await…which is also what the author is saying in the article.
I'm not saying anything about promises vs async/await. The original comment said that you can't have 2 async things happen in parallel without Promise.all, my code snippet proves that you can.
Node 16 will exit if an exception is thrown and not awaited for some finite period of time. So if your goal is to keep those promises in some cache and then resolve them later on at your leisure, you will find the entire node process will abort. There is a feature flag to restore the older behavior but it’s a pretty big gotcha.
There is no such finite period of time. You can call an async function and never await it.
Exception handling is something completely different. Yes, if you call an async function and do not catch the exception, Node will stop. But that is independent of having called await or not. Whether or not you await something async does not affect exception behavior.
Promises are also just syntactic sugar to make your code look more synchronous, you can do everything with plain callbacks. Which I find ironic with the article that he argues against that but still just stops at the next turtle, instead of following his own advice and actually learning how Javascript and it's runtime works.
It's easy to do two things at once when you can ask two different entities to do them for you (threads).
What's hard is thinking about how to coordinate the work they are doing for you: when to consider them done, how to ask them if they did the work successfully, what to do if they need to use the same tool at some point during the work etc.
Languages with threading require learning techniques to use them safely and many, including myself, have learned how.
Even if concurrency is easier to get right on node I'd say the node ecosystem has just layered on complexity in other ways to get to something just as difficult to use overall.
Promises and async/await sugar are only the tip of the iceberg.
It drove me crazy too, until I needed to use Puppeteer which requires you to write async/await (there are Puppeteer implementations in other languages, but they all seem to make compromises I didn't want). Generally speaking, async/await allows you to write code that looks and feels serial. Perhaps try using one of the async libraries for PHP to wrap your mind around the concept of async/await (like https://github.com/spatie/async)
The author even implies in a footnote that switch statements are unusable. I mean, we probably all had painful experience with the “gotcha”, and I appreciate efforts towards safer designs. But I mean, let’s not be ridiculous. It works fine.
what the author wants doesn't exists because the two saves will not actually run in parallel in any case.
Not with `async save(); async save();`
nor with `Promise.all`
nor with any callback or any other means.
the author is conflating parallel with concurrent programming.
and in (the mono-thread world of) javascript the two calls will still occurs sequentially.
Consider that 'save()' might do multiple steps under the covers (network, database, localstorage, whatever). Allowing those steps to run interleaved, if necessary (with Promise.all), might be quite different from serializing the 'save()' calls completely in the caller.
So while it is true that neither is truly parallel in the "parallel vs concurrent" sense, it is not true that the "sequential"/"concurrent" execution of both styles has the same performance characteristics.
If you're doing nothing in between your async calls, using .then/.catch might be simpler.
As soon as you need to introduce local variables and complex control structures, not having shared closures between your .then methods becomes extremely limiting. Hence, the async/await sugar.
About having to add await to ensure your error is handled with the try catch — not putting an await before a promise is something I use all the time. Being able to store tasks/promises and reuse/await them later is a clear advantage to other types of async code (such as coroutines).
Completely my feelings too. He doesn't actually address this, and it is actually the really painful part about promises which async/await makes infinitely better.
I have had cases where converting .then(...) code to async/await made the code infinitely easier to understand/reason about and made it trivial remove bugs which were present due to the complexities of dealing with control flow logic.
It strikes me that the author is just already "used to" promises and as such is trying to justify their preference for them with examples which they believe "prove" that promises are "better", but actually fail to do so. For every single one of their examples, async/await is no worse, if not better.
For example, their argument about inadvertently serialized work with the the two save calls. They are claiming that ".then()" is more obviously serializing than the "await" keyword, which is a highly subjective claim. If there is a problem here, it's that some developers don't understand/know about all the asynchronous tools which are available and so are unaware of the option of using Promises.all(...).
Another argument for async/await compared to promises is the following:
try
{
await doSomething();
}
catch()
{
// Do a particular error handling for doSomething() failing
}
try
{
await doSomethingElse();
}
catch()
{
// Do a different particular error handling for doSomethingElse() failing
}
What's the best way to reproduce this in promises only land, for example, does the following work?
doSomething()
.catch(() => {
// Do a particular error handling for doSomething() failing
})
.then(() => doSomethingElse())
.catch(() => {
// Do a different particular error handling for doSomethingElse() failing
});
I think it works, but I honestly don't know for sure offhand and to be sure I would need to check the documentation for promises, whereas for the async/await approach, there is no question. And if the above doesn't work, then I would have to call the .then() from inside the first .catch() block, which is awful code to read and interpret.
It's the same code. Async/await is just syntactic sugar over the promise syntax.
To me depends on what you are doing, there are cases where using .then()/.catch() and .finnally() makes the code more concise and simpler than using async/await.
.catch() returns a promise so you can call the then method on it again, yes. If you throw from either catch or then it will trigger the next catch method.
I agree. I think the article takes a simplistic view of the use cases in an effort to make the post consumable but in doing so, misses some of the point behind the syntax.
One thing I really like async/await for is you can use them in for-loops. It makes it significantly easier to do async work in what was previously very synchronous.
99% of the time you'll be storing promises in order to await on them in the same async method.
Those other 1% of times make for quite intuitive solutions, I think. For example, a cache. Most caches are request => already finished response, but with a lookup of promises you can easily do request => in-progress responses, for de-deplication.
Graceful shutdown & startup are two use cases that I used this for, just today.
When my express server gets a SIGTERM I want it to stop accepting new requests but finish pending requests before exiting.
Likewise during startup, the HTTP server is currently up before all resources are available - DB backends and the like. So I await a .ready promise before all requests iff we are not inited.
It can be abused into spaghetti for sure, but actual use cases are not that exotic imho
This article takes serious mental gymnastics to follow.
The main argument seems to be that an engineer with a working mental model for basic promise chaining will be unable to translate that to async/await, which is effectively syntax sugar for the same thing.
The author says that multiple calls to <promise>.then() is a cue that the code is occurring serially, and the new keyword await <promise> somehow isn’t.
There’s a time and place for raw promises, but this article hardly touches on anything actually wrong with async/await.
I’ve worked with engineers who actively fight against learning their tools, like this. It’s a nightmare. I don’t trust them. Don’t be that person.
- React hooks (which combined with Redux, React Query etc. is a special approach to async programming.)
I think it's very important to understand that ultimately, async (non-blocking) programming is kind of a "hard problem" and there is no trivial solution for it. But we can't get around it because we must not block code execution: Javascript runtime environments are single-threaded (except workers) and the JVM has limited thread count, too.
Any senior developer has to understand how each of these approaches work and how they are trying to solve the issue of non-blocking code execution. I dare say that this problem area is one of the hardest ones during becoming a more senior developer.
In terms of ergonomy (developer experience) when it comes to simple chained non-blocking calls, in my opinion nothing beats async/await in terms of syntax simplicity. I miss them from Java & Scala! No indentation, lambda functions, flatmaps, monads or special methods/hooks needed. You just have the result of an async method right there.
I don't agree with the author that we shouldn't use async/await because it _looks like_ synchronous code. We _should_ use it to keep syntax simple and clean wherever possible!
The developer has to properly learn the language fundamentals (and how JS code execution works) and know that async/await is just syntax sugar over Promises and generator functions. If they don't do this, then having a Promise.then() chain instead of async/await won't help that developer write better code.
On the other hand, once they understand it, they can freely choose between async/await and Promises depending on what needs to be written.
I think I understand you point when it comes to the essentially single threaded Javascript runtime, but the thread limit of the JVM is huge, so how does that matter?
Not trying to be pedantic, just wanted to know if I missed something.
Yes, for JVM, it's not that trivial and non-blocking code is definitely not mandatory. Our app backend has been written in a blocking fashion and we are gradually transitioning to non-blocking code.
Your question is very valid as the JVM can handle even thousands of threads (given the host OS can).
Having less threads does keep memory usage lower (as each thread in the thread pool has its own stack space). Also, it's more scalable because if using blocking code execution, you'll need to have one thread per concurrent request served. Lastly, thread starvation can become a problem with blocking code in case of delayed I/O or other failures. If that happens, the server becomes unable to serve new requests.
"To solve JavaScript's callback hell problem, several language mechanisms like Promise and async/await have already been introduced to JavaScript. Using async/await, which is the most promising one, callback hell code can be rewritten to another simple and shallow nested code with (almost) the same behavior. Unfortunately, however, it is still difficult to precisely understand the execution order of the rewritten async/await code, because the semantics of async/await is difficult.
This paper first clarifies that this problem is caused by the difficulty of the async/await semantics. Then, we propose and implement a novel async/await visualizer called AwaitViz, to support for programmers to understand the execution order of async/await. Our contribution is twofold. First, we show the feasibility of implementing the visualizer AwaitViz based on source-code instrumentation, which provides precise information on the JavaScript's asynchronous behavior. Second, we show the difficulties and limitations of implementing AwaitViz."
You can't talk about async await without talking about try, catch, and finally.
I don't think async+await+try+catch+finally is simpler than promises in any way.
I don't find anything simple or clean about imperative error handling.
I never liked promises when they were first introduced because I never perceived this "callback hell" to be an unmanageable problem; there are excellent libraries to manage callback composition, e.g. https://caolan.github.io/async/v3/
When Promises came along I didn't feel they added much value; in fact it made things more complicated (new paradigm, harder to compose).
Still, I went with it because everyone else went with it.
Then async/await came along, I had the same issues as described in the article. Most issues in the article I've made manageable (learning patterns over time, using libraries and/or conventions).
To me the article reads as someone who worked with async/await for some time and never wanted to embrace and work with them in the first place.
I'll be honest; if I'm working in a team with a collegue trying to force all code to use promises instead of async/await I'd probably ask him to stop doing that and escalate depending on the response.
Being idiomatic is very important, it makes code recognizable for newer devs. Had I stuck with callbacks until now I'd be writing code few young JS/TS devs would understand or like to work on.
The semantics of the library you link to seems fairly similar/analogous to promises to me! I'd in fact call promises themselves "an excellent library to manage callback composition", I don't see any reason to prefer the one you link over the promises api -- even if neither were built-in to JS. (And promises of course originally were not).
Forever, queue, series, times, retry, until, waterfall, whilst, etc etc... these are patterns that can all be achieved with promises too but I am sure most devs will do a google search before they do. In fact; some patterns are better served by a library.
Only just yesterday I added https://www.npmjs.com/package/p-limit to get concurrent promise limitation behaviour; one of the many features of that async-lib as well.
Don't misinterpret this as me suggesting these features should be added to the standard; far from it. Community libs do this job quite well.
I moved on from async, I moved on from Promises and embraced async/await.
Do I like it? No, I much rather use Golang's channels. But async/await is idiomatic and plenty of libs out there to handle complex use cases.
Almost everyone has moved on, and anyone writing promises at this moment is just creating legacy for anyone who is going to maintain it. Is that a good reason in itself? No, but it is a very valid one, that is my point.
Same here, I've never had that callback hell problem either, I think it's pretty elegant actually and you can do a lot of nice refactoring by extracting callbacks into variables. And the big strength is that your programming actually looks the way that the runtime works. It just becomes confusing with another abstraction that obfuscates the way the runtime works and requires you to now have this mental translation model.
Everytime I use a language with async / await syntax, I find myself wishing that awaiting async functions was the default behavior and that I had to specify when I didn't want that behavior. So much boilerplate, so many times I've seen bugs that are the result of not being aware of a function being asynchronous or that the function was later made asynchronous. Most of the time you want seemingly synchronous behavior within a request because it's easier to read, write, understand, and reason about and concurrent progress across requests. The places where I can overlap I/O within a request can be manually specified. I guess this is kind of how Go works (although that's parallelism and concurrency).
You may want to check out Zig's async/await. Seems to be closer to what you're suggesting. It's one of the few implementations where functions don't have an "async"/"no-async" color, and where you use the same IO functions regardless of whether you want to use them asynchronously or synchronously (if I remember correctly).
Kotlin coroutines do something like this. Any calls to a suspend function from inside another suspend function will be a suspension point and does not need additional syntax. And typically, if you are writing Kotlin using a JetBrains IDE the IDE will have a little indicator in the gutter to show all the suspend function calls.
To me personally it is much more readable with the gutter indicators instead of additional await keywords inside the actual code but it has a drawback: you need an IDE that supports it. When reviewing code on gitlab/github there won't be any indicator.
Heh I suggested this once on HN and got modded down to hell. I completely agree. I think any performance advantages of the async model are lost because it adds so much complexity to just reading and following code.
The amount of effort JS programmers have to go to in order to write synchronous code that is legible, works and easy to understand is enormous. I am a Bad Programmer so accept that some of it is just my lack of willingness to spend the time to get over that hump but I find it almost impossible to believe the gains are worth the costs.
I actually start to appreciate function coloring in C# to never forget that I am operating on asynchronous code. In .NET the compiler also throws warnings if you do not await or pickup the task/promise.
I also believe that you should see where asynchronous behavior can happen in your code. So from that angle i also appreciate the keyword and the coloring. Hidden resource access is pure evil.
The premise of this article seems flawed. We don't "code in a synchronous mindset" when we use async/await. I would expect most programmers are aware, after a 5 minute google search, that there is an asynchronous process happening and we need to ensure no execution after that line occurs until that process has resolved, hence we use await. If you know what it is doing, it's not complicated at all. It's identical to promise chaining but less ugly.
Ultimately the problem of potentially missed opportunities for parallelism there in regular sync code too. If we take the author's point seriously, we should all be programming in a language that requires explicit continuations everywhere just to guard against accidentally serializing things.
> It’s because we are taught to read async/await code in a synchronous mindset. We can’t parallelize the save calls in that first fully synchronous example, and that same — but now incorrect — logic follows us to the second example.
What a weird idea. When I see a serial bunch of awaits the first I think of is whether that makes any sense.
I think the author is kind of projecting his own views on everyone else here.
> Give better cues that we are in an asynchronous mental model
The `async` keyword(!) is objectively a clearer signal that the code in question is asynchronous. That's why type-checkers use it to prevent you from doing dumb stuff like awaiting in a synchronous function.
> In simple cases express the code at least as cleanly as async/await
It's pretty hard to see much of an argument here. How can the promise version ever be seen as "at least as clean"?
await someTask()
// vs
someTask().then(() => ...);
Even beyond the syntax here, the promise forces you into at least one level deep of nesting, which instantly makes it much trickier to manage intermediate variables, which either need to be passed along as part of the result (usually resulting in an unwieldy blob of data) or the promises need to be nested (pyramid of doom) so that inner callbacks can access the results from outer promises.
> Provides a much cleaner option for more complex workflows that include error handling and parallelisation.
If you've somehow come to the conclusion that `Promise.all` doesn't work with `async/await` then you have probably misunderstood the relationship between `async` functions and promises. They're the same thing. Want to parallelise a bunch of `await` statements? You can still use `Promise.all`!
I do occasionally find try-catch to be awkward, but that's because it creates a new lexical scope (just like promise callbacks do). I also think the consistency from having one unified way to handle errors in sync/async contexts justifies it.
Regarding the error handling case, I've played around with the idea of implementing a Result type with typescript and hiding the errors behind that. The end result looks something like this
const res = await getJSON('/thingy');
if (isErr(res)) {
// Handle error.
return;
}
I always wondered why async/await introduced a concise way of dealing with asynchronous code, but then breaks all this conciseness with try/catch ...
Hence I tried to combine async/await with catch for an improved readability [0] two years ago. Turns out I never used this approach, because it's not common sense when working in a team and still feels too verbose. Either one uses then/catch or async/await with try/catch. However, I still feel try/catch still makes code unattractive.
I've never found this to be much of a problem in good code. API failures in front-end code tend to fall into a couple of buckets that can be abstracted away. If you want to preserve the page and keep retrying with exponential backoff, you can just implement that as a function that you then await until it succeeds. If you can't recover and just want to show the user a "shit's fucked, refresh the page" banner then you just need one handler near the top of the stack and let the errors bubble up. Most people are working in a declarative environment like React where you can just have a hook wrapping your API calls that exposes a potential error state to render accordingly.
You only really need to have try/catch blocks in 1 or 2 places unless you have a lot of novel async stuff going on that needs to recover from failures in specific ways. Most front-end code shouldn't have a try/catch around every API call.
I try to read every one of these sorts of articles that comes up. I don't want to be caught flat-footed on some kind of weird memory leak issue from not using a pattern correctly, or something.
But every single one of these articles that I've read against async/await has been nothing more than inventing "problems" to try to convince people to stick to raw promises strictly because that's what the author is comfortable with using. No actually technical merits are discussed. Just fantasy issues so author can feel superior for not learning something new.
Like how the author claims you can't visually "see" that two await calls can be parallelized. Speak for yourself, buddy. And then complains about "having to go back to using promises" to affect the parallelization. My dude, it was promises all along.
It's just syntax. Use it, don't use it, mix and match it. It's not a moral issue.
> you can't visually "see" that two await calls can be parallelized. Speak for yourself, buddy.
Agreed. My first thought was to try
var userSaveTask = save('userData', userData);
var sessionSaveTask = save('session', sessionPrefences);
await Task.WhenAll(userSaveTask, sessionSaveTask);
I saw it before I got to the part of the text explaining how I wasn't going to just see it.
> Furthermore, if we are to take advantage of parallelization in the async/await example, we must use promises anyway.
Again, Speak for yourself, buddy. "await Task.WhenAll" does that job in c#, and there must be a JavaScript equivalent.
IDK, this may be the c# mindset. yes, "async / await" is an extra-ordinarily complex feature that can easily be done wrong. But its also very useful and powerful, so lets not throw it out.
> Like how the author claims you can't visually "see" that two await calls can be parallelized.
I'm not gonna pretend to be a JS expert here, but isn't that exactly what you can see? Isn't that the whole point? It lets you easily take a second look and decide to check if the operations can happen in parallel or not.
I'm literally using 2 awaits in a row because android's BLE implementation doesn't necessarily like more than 1 operation happening at once, and I'm not doing enough other operations such that I need to implement a whole queue to handle it.
(although maybe the library i'm using should implement that queue)
In that first example, `await` means exactly the same thing as `then` for the purposes the author is talking about -- realizing that something had been serialized that could have been done in parallel.
I don't see why most engineers would be likely to recognize that opportunity when written as `then` but not as `await`. If is is true (and I'm not convinced it is), it seems like an education problem rather than a reason to use promises rather than await. It doesn't seem fundamentally harder to know that two `awaits` in a row means serialization than to know that two `thens` in a row does.
At least in that simple example where the use of `then` is exactly parallel to the use of `await`. Perhaps it wasn't a good example, and it really would be harder to reason about in a more complicated example.
Their promise-based error handling seems to skip over a major gotcha: if the first `save` call throws an exception, it does not get handled in the `.catch()` callback! It would require its own try-catch to handle. That is an overlooked benefit of try-catch in async functions: it handles everything, both normal exceptions and promise rejections.
Author mentions that async/await messes up with mental model of the code, and I think that's the most important issue with it, but it goes even deeper than described in the article.
There are no inherently async or sync functions. It's not a property of a function, rather the property of what caller does after calling a function.
Is throwing a ball an async or sync action? Well, if tennis robot machine spits one ball after another and doesn't care/wait about feedback – then it's async. If the tennis player hits the ball with a racket and puts all her/his attention into waiting/validating the result (essentially blocking) – then it's synchronous function. It's essentially an "attention" of the caller that defines sync or async.
Marking code as inherently "sync" or "async" or claiming that one functions are "hard" and others are "easy" is the single dumbest idea I've seen in computer programming. And it's insane how contagious it is - seems like languages are more often copypasting features from other, instead of designing from the first principles.
Since this article is specifically about Javascript: I think the implementation of async/await in the language is heavily constrained by a desire for backwards compatibility.
After all, async/await is ultimately promises all the way down, so you can happily write code and let a compiler turn it into ES5 that runs anywhere.
If instead JS allowed you to shunt arbitrary function calls off into their own threads, then you would need bigger changes to the engines to support that, and you couldn't back-port that to vanilla javascript.
To be honest, I find promises even worse concept for concurrent programming. At least I never think about things and behaviour in terms of "objects that will yield value in the future". To use promises I need to build a layer of conversion between "how my brain thinks about world" and "passing around promises" – and that's just textbook accidental complexity and unneeded cognitive load.
The least cognitively expensive model for concurrent programming is CSP, precisely because it fits into how we humans reason about world.
Me too, there is no concurrent programming in Javascript. It's a continuation passing style language and a single threaded runtime, I wish people would just spend a little time learning how that works and normal javascript will make a lot of sense and not look "yucky" anymore. And you will realise how great of a fit that is for UI programming and reacting to events from the user, where you don't have to think about blocking the UI thread like you need to in Java.
Agree that try/catch is verbose and not terribly ergonomic, but my solution has been to treat errors as values rather than exceptions, by default. It's much less painful to achieve this if you use a library with an implementation of a Result type, which I admit is a bit of a nuisance workaround, but worth it. I've recently been using: https://github.com/swan-io/boxed.
By far the greatest benefit is being able to sanely implement a type-safe API. To me, it is utter madness throwing custom extensions of the Error class arbitrarily deep in the call-stack, and then having a catch handler somewhere up the top hoping that each error case is matched and correctly translated to the intended http response (at least this seems to be a common alternative).
I don't get it then; your fix to promises being complicated is to remove the ability to perform asynchronous actions. That's cool for a small scripting language I guess, but absolutely impractical for anything serious.
hyperscript is designed for small, embedded front end scripting needs: toggling classes, listening for events from its sister project, https://htmx.org, etc. It isn't a general purpose programming language for use, for example, on the server side in a node-like environment.
you can still perform things asynchronously by wrapping any expression or command in an `async` prefix:
but there isn't a mechanism for resolving all of them
although, now, come to think of it, the following would work:
set results to {result1: somethingThatReturnsAPromise(), result2: somethingElseThatReturnsAPromise()}
That would work out because under the covers the hyperscript runtime calls a Promise.all() on those field values before it continues. Kind of a hack, but it would work.
Anyway, again, hyperscript is a DSL targeted at small front end scripting needs rather than being a large scale concurrent systems programming language.
YOU may, buy your core frameworks may not. And if your synchronous code is holding up async loops, you're gonna potentially create a lot of very hard to diagnose problems. Beware the foundation on which you build.
> a try block in JavaScript immediately opts that section of code out of many engine optimizations as the code can no longer be broken down into determinative pieces
This isn't true and the dependent clause doesn't even make sense.
Yes, V8 had trouble optimizing try/catch back in the Crankshaft days. But those days are long gone, and major engines (V8/TurboFan, JSC, and Mozilla's latest *Monkey) handle this construct quite well. Even if this were not true, it would be meaningless, as this is simply syntactic sugar over `Promise#catch()`, which you would be using anyway.
I’d like to see the author rewrite a for-of loop that contains async code in the body as traditional promise code, then look me in the eye and tell me the for-of version is more awkward.
For me, the key to async/await is a linter that complains about un-handled promises. I appreciate in Go that any green threads are created explicitly with `go`, whereas in typescript, they are signaled by the return type.
My most recent background was in Go and so my mental model now is that async functions are similar to goroutines and `await` is a nice result collection mechanism.
What's recently confused me is that exceptions from yet-to-be-awaited promises crash if you await anything else first.
Async / await is nice syntactic sugar. I don't understand why they copied the naming from c# over do syntax in Haskell (which dates roughly 1995, if I'm not mistaken).
The main problem is error handling, having to use try / catch is pure cancer and really screw up your closure.
You can use catch together with await / async but then you're not dealing with exceptions outside the promises.
1. do notation works and is used for any monadic type (e.g. lists, parsers, promises, resources like database-connection-contexts, ...) while async/await works for promises only
2. async works on the function-level and only there while do-notation can be used in any place where a normal expression can be used, including being abitrarily nested.
There are more differences, but that should be enough food for the mind to think about it.
You could summarize it as: async/await makes it easier to work with promises on the syntax level and trying to make async code look like sync code, while the do-notation does not try to make async code to look like sync code, but rather embrace that sometimes two different semantical flows are interwined (one inside a monadic context, one pure) and makes it easier to work with them on the syntax level while at the same time making the two look _different_ explicitly.
From the perspective of someone wanting to use the powerful do-notation, sure. But at the same time, the hurdle for many developers who just want to "use promises with ease", it would have been a steeper learning curve with much less beginner-friendly syntax.
do-notation doesn't really help to deal with promises, it even makes it harder because it is confusing at the beginning. Async/await makes using promises easier and hides problems for some time at least. I can see why they chose to use async/await.
Just to give an example: error handling with try/catch. That doesn't work with do-notation, but with async/await you can integrate it (more or less at least) and it looks like sync code.
Typescript and C# are both developed and maintained by Microsoft, and the lead architect of C# is also the creator of Typescript. (Both great languages, IMO)
Async/Await covers the 80% of use cases for async logic in JS. Most people aren't really using promises as multicast references. They don't call `then` in one place, hang on to the promise reference, then call `then` again somewhere else (perhaps to represent a cached value); they call `then` once on the reference because it's just a moment in a composite operation.
It's for this reason that I think this library[0] is the more appropriate abstraction for that same 80% of use-cases, as its more memory efficient since you can represent the same composite operation that generates multiple promise references with a single object (a unicast reference instead). I haven't learned Rust but apparently the author bases this on Rust's ownership principle.
Async/await optimizes for throughput, not latency. Use case is many lightweight tasks you want to parallel as wholes. Web APIs, web sites are usual examples.
However, if you need other kind of optimizations, like two parallel saves within one request, maybe async/await is not what you need in the first place.
But the async/await/Promises only make the code look synchronous because it actually makes the code synchronous. The await literally blocks the current thread, it's nice that the code is executed in a thread scheduler but I'm still blocking the current thread. How this was accepted as a big win is not clear to me. If you want to make it asynchronous you still have to write .Then(()=> {}) which is a nested function. So we won nothing on that front. It's literally just about taking tiny pieces of code out and running them in a different thread... while the current thread is blocked anyway. So all this hurrah for almost nothing gained. In C# we already had a task scheduler ala Task.Run(()=>{}). Async instead of Task.Run() seems like such a microsopic win in exchange for an enormous amount of complexity.
I thought "async io" was supposed to be superior to "blocking the thread". The unit of computation is "smaller" than an os thread, right? It's cooperative multitasking, where thread-per-task is not cooperative. Maybe you were using "thread" symbolically?
I understand there are two sides/groups here: 1) Those who think errors are important to control flow, and 2) Those who think errors are exceptions to control flow. If you are in group 2 both Promises and async/await will give you neat and simple code. But if you are in group 1 Promises and async/await will be really complicated and ugly because each await will be inside a try/catch.
Because I'm in group 1, I try to avoid Promises (and thus async/await) in JS because the promise will capture all future errors, and if you forget to add a .catch, that error will never surface. Promises make asynchronous code more complicated. Async/await however get rid of a lot of the complicated syntax, so when I do not care about errors (eg. errors are exceptions) I use async/await because of easier control flow.
Those who think of errors as exceptions can further be divided into group 2a) Those who prefer Promises syntax. And group 2b) Those who prefer async/await syntax.
Also don't forget about co-routines:
co(function* () {
var user = yield getUser();
var comments = yield getComments(user);
});
Just replace "co(" with async, and yield with await, and you'll have async/await.
I don’t think of async await as “making async code act like sync code”.
I see it as adding meta data to a function type (async) and allowing you to attach the result to a given function scope (await).
The key utility of this is to allow you to use functions as a unit of composition, allowing you to leverage their already built tooling (IDE jump to function, stack frame based debugging, input and output types (in:args -> out:return).
Messaging runtimes like Golang and Erlang use an additional composition unit (channels and mailboxes), and do not allow you to follow the “tree of functions” paradigm like async await does. These result in having to follow a network graph of messages and nodes to find out what will happen.
You need to understand you are dealing with an event loop before you use async await. I think this is the issue the author is raising.
Some language features aren’t aimed at toy examples, and if that’s the scope of your thinking, you won’t see the point.
For example, I like how the catch block is just a single function call to handleErrorSomehow — which would be totally fine for an example, if it weren’t for the fact that the author makes a special note of how convenient it is that the Promise variant reduces to just .catch(handleErrorSomehow). Suuuper-realistic.
I should probably admit that I also didn’t completely see the point of async/await — this was years ago in C# — until I first had a chance to use it inside a
complex loop of some kind. I think it’s a good exercise to try manually desugaring such an example. It really makes you appreciate what the compiler’s doing for you in these cases.
Very strange to compare async/await to promises when it is just a part of promises. There are rare situations where promise callbacks might be easier to read but those are more edge cases and a matter of personal preference.
We need an AI to design a language that is optimal for humans to program with. Languages should be designed by behavioral scientists rather that computer scientists bcause their goal is to adapt to the human rather than the machine. For example one of the biggest limitations of humans is limited capacity of working memory. Async paradigms exhaust this limit very early and lead to frustration compared to serial programming. Writing one of them in the form of the other doesn't solve any problem and is indeed a travesty
All async/await is built on promises/futures/tasks, which themselves are built on yield/generators. Most modern languages have converged on this and they are not different paradigms but rather just newer syntax.
Async/await is better for expressing concurrent logic more tersely and in the common "sync" format, and failure to understand what it means is just that, a failure in understanding.
There's no convincing argument here to use "promises instead async" because it's the same thing.
Async/await was the latest buzzword a few years back, but it never had any substance. A lot of inexperienced programmers thought it was the greatest thing ever. Unfortunately, they were overly focusing on the look of the code, and not the true readability or usability of the code. Async/await certainly makes you code "look" synchronous, but it's just shuffling the complexity around. The complexity is still there, and you will have to think about it.
I don't mind using async/await when I use promises, but in my experience people who never worked with bare promises have a hard time really understanding async/await and will eventually need help from people who did for harder problems. Just like people who never used callback flows often struggle to understand well asynchronicity and mess up with promises.
It's interesting using async/await, setting the target ES to be an older version and watching how the TypeScript compiler replaces async/await with yields and then no yields but a ton of scary looking boilerplate as you progress further back through older versions of ES.
Await basically splits function in two functions (before and after) and uses second function as a callback for promise then. Error handling makes it a little bit more complex, but not much. Babel seems to make it harder than it needs to be, but may be that makes sense from performance or maintainability perspective.
I understand that the author is more familiar with the syntax of promises. Nonetheless, having a syntax similar to synchronous code allows developers to write more code remembering only the couple of changes in syntax instead of having to memorise a different one.
JavaScript Promises have their issues - the same ones all asynchrony-providing libraries do. I feel the author takes a hard stance on something quite nuanced.
Syntactic sugar hides the API, making it more ergonomic, also leaking those problems. Same problem, different API
the whole blog-post seems a little constructed tbh - If I look at
await doSomething(param)
await doSomethingElse(param)
the very first thing that comes to mind is that this can be very, very easily optimised by just doing await Promise.all([
doSomething(param),
doSomethingElse(param)
])
This is something I come across literally every day - why exactly would that be an argument against async/await? much so on the contrary, I find const myResult = await Promise.all([]) significantly more expressive, than Promise.all([]).then(doSomething)
Also, as others have already pointed out - async/await is just syntactic sugar around promises and can be used in addition to 'normal' promise syntax (as shown above)
This take is garbage imo. Coming from someone who first used jquery deferred, then native promises and now async await. If you are too inexperienced to use await properly no reason to think callback hell is going to make it better
Task { [weak self] in
await foo()
self?.bar() // self may be nil
}
That is, you can just make a new task which awaits on something and then does more work, and that Task can be tagged with weak self so that self can be freed.
If you’re await’ing a variable directly in the same scope, of course self will be retained, you’re still running inside self’s scope.
I guess I’m confused by the question… if you want to await something and allow self to be freed in the process, you can use Task{ [weak self] } and then return early. If you don’t want to return early, you can’t really release self yet, since the scope shouldn’t outlive self.
Promises are the most awful programming syntax I have ever come across.
Await is sweet release from death as nowadays almost all apis are forced onto promises. Thank god xhr and websockets came before promises were mainstream, but for example webserial is absolutely horrible to use.
Function callbacks are the best, though I wish JS would support function scheduling.
Long rant is long, so I’ll leave a tl;dr: regardless of how you feel about the syntactic sugar aspect, async/await has a runtime advantage of suspending the stack rather than exiting early, which is (IMO) of greater benefit than the syntactic improvement.
- - -
I’m in the process of gradually moving several decade-old JS projects from explicit Promise APIs to async/await. The reason for this is not the syntactic sugar (although that’s quite a benefit, and I disagree wholeheartedly with the article on that point). The reason is that, at least with native Promises, the actual runtime behavior of async/await is considerably better. Because await suspends the stack, where .then/.catch enter a new stack frame.
I do see this has been discussed some in another thread, but I feel like it deserves more direct attention. It’s true that you can use libraries like Bluebird or whatever to get more accurate stack traces. But this is done by tracking Promise chains in user space. This is fine but it comes with a cost. Even if the library is quite fast (as Bluebird is), you still incur the cost of downloading and parsing it. Especially on HN where JS bloat is a daily topic, I’d hope this can be appreciated.
In my case, this cost wouldn’t be worth it even if the Promise API were clearly better. These projects need to run efficiently on low power mobile devices with limited connectivity. And we need to be able to diagnose runtime errors to address outstanding bugs. A library is a non-starter, and native Promise APIs actively hinder progress. The behavior of async/await is clearly superior.
Suspending the stack, either with async/await or generators, affords significantly better debugging capabilities. It allows Error stacks and console.trace to take better advantage of source maps.
It‘s also a much better optimization target for browsers. This isn’t just academic: even if async/await is semantically equivalent to Promise, it’s not a guarantee that the underlying .then/.catch will even be called if there’s no explicit Promise in the code. I’ve seen cases where they’re not, which was baffling until I understood (and this is how I came to understand) the difference in runtime behavior.
Sure async/await makes your casde more complicated .
That reason why, I prefer use Go for I/O wait application because I don't need to manage this Go doesn't it fore me.
My code still readable and simple.
I kind of agree. I learned promises from the promise API - flatmapping over things, that will happen when the thing is resolved.
I remember async/await was harder to grok because it seemed so arbitrary. A bunch of syntax sugar, and function decorations.. promises were just method calls.
A lot of the argument appears to be the author extrapolating from his own lack of familiarity to others: "We are taught", "our minds", etc. I can easily construe some hypothetical person with a certain set of skills (and lack thereof) that would have just as much trouble with bare-bones Promises. But I don't have to because the author did it for me at "One more thing…".
"People can mess this up" was never an argument. You can mess everything up. The more interesting question is how badly and how often.