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.
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.
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.
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).
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.
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
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.
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.
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)
- 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.
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.
"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."
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.
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 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)
> 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.
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.
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.
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.
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.
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.
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.
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?
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 )
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.
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.
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.
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).
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.
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.
> 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.
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.
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.
> 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.
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)
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.
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.
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)
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.
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.
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.
> 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.
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.
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.
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.
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.
241 comments
[ 3.4 ms ] story [ 257 ms ] threadIf the part of the function after await is the callback, how do I specify weak self if I need to?
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.
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).
This sounds like horrible spaghetti. I can understand you might need to do it in exceptional circumstances, but I wouldn’t make a habit of it.
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.
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
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:
What's the best way to reproduce this in promises only land, for example, does the following work? 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.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.
Doesn’t it remove any opportunity for parallelism, making the loop take much longer than it would with a Promise.all?
Or am I missing something?
If you want parallelism: await Promise.all(x.map(...))
To each their own!
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.
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.
- Spring MVC (Java Futures)
- Spring Webflux (Reactor observables)
- Scala (Scala Futures & for comprehensions)
- TS async/await
- Angular observables
- 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.
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.
See this SO answer: https://stackoverflow.com/a/63490797
I don't find anything simple or clean about imperative error handling.
Not having code that is actively deceptive sounds like a good idea to me, so agree with the author.
> We _should_ use it to keep syntax simple and clean wherever possible!
Having clean and simple syntax for async operations is a laudable goal.
Having code that is actively deceptive about its semantics seems less than ideal for accomplishing this goal.
Maybe we need to figure out new and different syntactic constructs that are clean and simple, but do not pretend to be something they are not?
https://dl.acm.org/doi/10.1145/3297280.3297528
"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."
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.
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.
Agreed. My first thought was to try
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.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
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)
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.
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.
[0] https://www.robinwieruch.de/javascript-async-await-without-t...
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.
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.
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.
For starters you can just write:
orOf course in newer versions of node we can now do:
const wait = require(‘util’).promisify(setTimeout)
The code above behaves exactly the same as
or even just> usually junior front-end
So you agree with each other then?
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.
That is preposterous.
Would you say the same thing about
function getData() { return mysteryFunction(…); }
[0] https://www.youtube.com/watch?v=8aGhZQkoFbQ
[1] https://javascript.info/async
[2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guid...
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.
Not a popular opinion I know, but there you go.
Wouldn't you simply be reinventing promises?
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.
That is NOT what async/await is about — after all, async explicitly marks functions as asynchronous. Not exactly trying to hide that.
Async/await is syntactic sugar. It is there to make working with Promises less verbose.
That is almost all: await pauses current function execution in the main event loop, which is an important detail
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.
Promise.resolve()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
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.)
----------
Promise.all()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
For making requests in parallel, and returning when all have been successful.
----------
Promise.allSettled()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
For making requests in parallel, and returning when all have completed whether successful or not.
----------
Promise.any()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
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 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).
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.
The only time I feel that promise chains are better async/await is when treating Promise as a monad https://blog.bitsrc.io/out-with-async-await-and-in-with-prom...
This is literally 97.5% of all tech blogs, and a disappointing amount of content on HN front page.
I feel bad for the author. Nothing like asserting your ignorance on a blog. There's a time and a place for literally every language construct.
This article is bad, bad advice.
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.
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.
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.
[1] https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
[2] https://vorpus.org/blog/some-thoughts-on-asynchronous-api-de...
[3] http://erights.org/talks/thesis/index.html
[4] http://erights.org/elib/distrib/pipeline.html
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.
Callbacks were absolutely unmaintainable. Promises (and `async/await`) are really not that hard.
Promises make the most horrible spaghetti code there is. It is hard to read, hard to maintain and hard to reason about.
Callbacks are super simple and clear, both as an api and how data is passed.
And these days I do the same when I want to simplify WebWorker communication. Promises are great
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.
Just to be clear; I would never use that lib in any circumstance right now, I'd always prefer async/await for idiomatic reasons.
But please have a look at http://caolan.github.io/async/v3/docs.html#controlflow
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.
All of this author's examples using .then() are single-line functions. Seems contrived to suit their opinion.
Just use the right tool for the job.
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"?
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.
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)
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.
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.
Makes using a bit of JavaScript relatively simple, just not much in Stack Exchange yet which means reading docs..
Learning the event loop, then promises, then async/await is a must. Today, you probably should throw typescript on top.
A steep learning curve just to get back to a typed language that can do things concurrently.
You do get used to it, but it is a mess of stuff.
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.
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.
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.
Both those promises start, and both are waited for after both have started..
That is the same as promise.all... There's just an explicit order for the wait, rather than as they resolve, but the result is the same.
Now, promise.any.... You'd have a point...
If the first await is the slowest, the second one will return immediately (like calling .then on an already resolved promise).
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.
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.
In fact, my immediate intuition with the await examples was to parallelize with 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:
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.
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.
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.