I Avoid Async/Await
uniqname.medium.com
uniqname.medium.com
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.
async function() {}
is much nicer than function() {
return new Promise(
(resolve) => resolve()
)
}For starters you can just write:
const myfunc = () => new Promise(resolve => resolve())
or Promise.resolve() 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? const wait = (ms = 500) => new Promise(resolve => setTimeout(resolve, ms));
then: async function () {
await wait(1000);
}
Perhaps it's hard to grok but it sure is a handy one-liner.Of course in newer versions of node we can now do:
import {
setTimeout,
} from 'timers/promises';
await setTimeout(100);const wait = require(‘util’).promisify(setTimeout)
async function process() {
return new Promise((resolve, reject) => {
emitter.on(”error”, (err) => reject(err));
emitter.on(”end”, (result) => resolve(result));
emitter.start();
});
}
async is not strictly needed here, but I find it a good practice to use it on functions which immediately return a Promise anyway. 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. return await foo(); async function getData() {
return new Promise((resolve) => {
const res = fetch(…);
resolve(res);
});
}
The response above is wrapped in THREE different Promises! One from fetch, one manually created, and one implicitly created by `async`.The code above behaves exactly the same as
function getData() {
return fetch(…);
}
or even just fetch(…);> 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...
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.
const x = somethingAsync();
const y = somethingAsyncToo();
return { foo: await x, bar: await y }
There is no point in returning one before the other because you need both?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.
await Promise.all([/* build promises */]);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.
const fooP = fetch(a)
const bar = await fetch(b)
const foo = await fooP async {
save()
save()
} catch (Exception e) {
console.log("Handle error")
}
async does not deliver this at all.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.
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:
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.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 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.
- 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."
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.
https://typescript-eslint.io/rules/no-misused-promises/
https://typescript-eslint.io/rules/no-floating-promises/
They can catch some common mistakes. Of course these checks can only work reliably when you're using a typed language.
I'm still looking for one that forces me to put an await in front of every function call that returns a promise, unless I opt out.
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 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.
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.
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.
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.
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.
const res = await getJSON('/thingy');
if (isErr(res)) {
// Handle error.
return;
}
You can read the details from here: https://thingsthatkeepmeupatnight.dev/posts/simple-typescrip...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.
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
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.
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)
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.
Tell me you're a difficult teammate without saying you're a difficult teammate.
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.
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.
The least cognitively expensive model for concurrent programming is CSP, precisely because it fits into how we humans reason about world.
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).
https://hyperscript.org/docs/#async
we call it "Async Transparency"
It lets you write code like this:
fetch /some-url as json
put the result's content into me
where fetch is an async call, but you don't have to await it or anything or mark it as being an async fuction, whathaveyoueffectively, we de-color the language:
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
my feeling is that async concerns are too low level for light scripting
fetch /some-url as json
fetch /some-other-url as json
do_stuff_with(result1, result2)
in parallel?If I needed that I would kick out to javascript and use Promise.all() to return an expression that hyperscript could then sync on
the wheelhouse for hyperscript is stuff like:
on load
wait 5s
transition my opacity to 0
remove me
where you don't have to do any async or callback stuff for what are, at root, async operations.you can still perform things asynchronously by wrapping any expression or command in an `async` prefix:
https://hyperscript.org/commands/async/
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.
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.
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.
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.
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.
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?
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.
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 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.
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.
By using async/await, we are inherently limited by the flow control because we are forced into await resolving promises.
This is why libraries like redux-saga[0] or cofx[1] use generators.
https://redux-saga.js.org https://github.com/neurosnap/cofx
Generators provide much better flexibility over flow control and allow us to treat side effects as data.
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.
It does.
Syntactic sugar hides the API, making it more ergonomic, also leaking those problems. Same problem, different API
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)
If the part of the function after await is the callback, how do I specify weak self if I need to?
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.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.
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
await Promise.all(…)
And now?- - -
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.
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.