Remix.run Logo
serbuvlad 7 days ago

As someone who has only written serious applications in single-threaded, or manually threaded C/C++, and concurrent applications in go using goroutines, channels, and all that fun stuff, I always find the discussion around async/await fascinating. Especially since it seems to be so ubiquitous in modern programming, outside of my sphere.

But one thing is: I don't get it. Why can't I await in a normal function? await sounds blocking. If async functions return promises, why can't I launch multiple async functions, then await on each of them, in a non-async function that does not return a promise?

I get there are answers to my questions. I get await means "yeald if not ready" and if the function is not async "yeald" is meaningless. But I find it a very strange way of thinking nonetheless.

tbrownaw 3 days ago | parent | next [-]

The `await` keyword means "turn the rest of this function into a callback for the when the Task I'm waiting on finishes, and return the resulting Task". Returning a Task only works if your function is declared to return a Task.

The `async` keyword flags functions that are allowed to be transformed like that. I assume it could have been made implicit.

You can do a blocking wait on a Task or collection of Tasks. But you don't want to do that from a place that might be called from the event loop's thread pool (such as anything called from a Task's completion callback), since it can lock up.

captaincrowbar 2 days ago | parent | next [-]

"The `await` keyword means "turn the rest of this function into a callback for the when the Task I'm waiting on finishes, and return the resulting Task"."

Oh my god thank you. I've been trying to wrap my head around the whole async/await paradigm for years, basically writing code based on a few black magic rules that I only half understand, and you finally made it all clear in once sentence. Why all those other attempts to explain async/await don't just say this I can't imagine.

quectophoton 2 days ago | parent | next [-]

> Why all those other attempts to explain async/await don't just say this I can't imagine.

On one hand, the people who write explanations are usually those who have lived through a language's history and learned things gradually, and also have context of how things were solved (or not) before a feature was introduced.

On the other hand, the people who are looking for an explanation, are usually just jumping directly to the latest version of the language, and lack a lot of that context.

Then those who do the explaining underestimate how much their explanations are relying on that prior context.

That's why I think the ideal person to explain something to you, is someone with an experience as similar as possible to your own, but also knows about the thing that you want to know about. Because this way they will use terms similar to those you would use yourself. Even if those terms are imprecise or technically incorrect, they would still help you in actually understanding, because those explanations would actually be building on top of your current knowledge, or close to it.

This is also why all monad explanations are... uh... like that.

xiphias2 2 days ago | parent | prev [-]

It's because implementing this is not that easy: there are differences between the implementation of coroutines and await that makes it tricky (especially waiting for both CPU tasks and network events).

For Python I loved this talk by David Beazley:

https://www.youtube.com/watch?v=MCs5OvhV9S4&t=2510s

He's implementing async/await from coroutines ground up by live coding on the stage

ivanjermakov 2 days ago | parent | prev [-]

> I assume it could have been made implicit

Not quite. It gets ambiguous whether to wrap return or not. Example:

    function foo(): Promise<number> {
        if (...) { return Promise.resolve(5) }
        ...
    }
but async version is:

    async function foo(): Promise<number> {
        if (...) { return 5 }
        ...
    }
Although you can bake into the language one way or another.
mrkeen 7 days ago | parent | prev | next [-]

You can:

https://hackage.haskell.org/package/async-2.2.5/docs/Control...

As long as you don't mind - what did the article say? -

>> transcending to a higher plane and looking down to the folks who are stitching together if statements, for loops, make side effects everywhere, and are doing highly inappropriate things with IO.

do_not_redeem 3 days ago | parent [-]

[flagged]

binary132 3 days ago | parent | next [-]

Thank goodness Captain Imperative scampered out of his goto hovel to remind us wizards to feel bad for being better than everyone. ;)

do_not_redeem 3 days ago | parent [-]

Your purely functional insults have no side effect on me, wizard.

binary132 2 days ago | parent [-]

Argh, right in the monads!

zeendo 3 days ago | parent | prev [-]

What is with the unprovoked condescension?

I guess I get the article author was trying to be provocative but what are you doing?

mrkeen 2 days ago | parent [-]

To lay it all out in full:

TFA wrote out a bunch of problems with imperative code asked you to take it on faith that imperative is not inferior to functional. Then he added a bunch of snark, implying that functional programming is just a superiority complex (which is what I quoted).

I quoted the snark to indicate that I was very aware that the author was mocking FP.

serbuvlad asked why you can't just X. I linked to the exact function that does that for you.

do_not_redeem then suggested I was unaware that the author was mocking FP.

the_mitsuhiko 2 days ago | parent [-]

FWIW I (author) didn’t want to mock functional programming. I consider myself an aficionado of functional programming patterns :)

skybrian 2 days ago | parent | prev | next [-]

Ultimately, it's because of how JavaScript models time.

In a browser, JavaScript has a single event queue, and events are delivered one at a time. Event delivery is supposed to be fast. Conceptually, handling one event can be seen as a single tick of a clock, and practically, it should be less than one animation frame, so the UI doesn't freeze. If something takes time, you need to split it up over multiple events, so that it spans multiple clock ticks.

An async function call is one that can return in a different clock tick. A "normal" JavaScript function call always returns in the same clock tick, and if it takes too long, it makes the clock tick take longer, holding everything up until it's done.

This is a logical consequence of JavaScript starting out single-threaded and being used to write UI event handlers. In other languages, there isn't this distinction. Assumptions about event handling aren't baked so deeply into the language, so normal function calls in other languages don't make this guarantee about happening atomically.

shepherdjerred 3 days ago | parent | prev | next [-]

At least in JavaScript, you could mark all of your functions as `async`.

This would mean that function would have to return a Promise and go back to the event loop which would add overhead. I imagine it'd kill performance since you'd essentially be context switching on every function call.

The obvious workaround for this is to say "I want some of my code to run serially without promises", which is essentially is asking for a `sync` keyword (or, `async` which would be the inverse).

AlienRobot 3 days ago | parent | prev | next [-]

In my experience with web browsers, you can't do this because Javascript can NEVER block. For example, if a function takes too long to run, it blocks rendering of the page. If there were ways to make Javascript asynchronously, browsers would have implemented it already, so I assume they can't do it without potential backward incompatibility.

One exception is alert(), which blocks and shows a dialog. But I don't think I've ever seen a website use it instead of showing a "normal" popup with CSS. It looks ugly so it's only used to debug that code actually runs.

I'm not knowledgeable about low-level interruptions, but I think you would need at least some runtime code to implement blocking the thread. In any case, even if the language provides this, you can't use it because the main thread is normally a GUI thread that can't respond to user interaction if it's blocked by another thread. That's the main point of using (background) threads in the first place: so the main thread never blocks from IO bottlenecks.

davnicwil 2 days ago | parent | prev | next [-]

I get it - you'd like await semantics in a function without having to expose that detail to the caller.

You can't get it directly in javascript but you're only one step away. Just not awaiting your function in the caller 'breaks the chain' so to speak, so that at least the caller doesn't have to be async. That way you can avoid tagging your function as async completely.

Therefore one syntax workaround while still being able to use await semantics would just be to nest this extra level inside your function -- wrap those await calls in an anonymous inner function which is tagged async, which you just instantly call that without await, so the function itself doesn't have to be (and does not return a promise).

binary132 3 days ago | parent | prev | next [-]

I found it all very confusing until I eventually wrote a little async task scheduler in Lua. Lua has an async / cooperative-coroutine API that is both very simple and easy to express meaningful coroutines with. The API is almost like a sort of system of safer gotos, but in practice it’s very much like Go channel receives, if waiting for a value from a channel was how you passed control to the producer side, and instead of a channel, the producer was just a function call that would return the next value every time you passed it control.

What’s interesting is that C++20 coroutines have very nearly the same API and semantics as Lua’s coroutines. Still haven’t taken the time to dive into that, but now that 23 is published and has working ranges, std::generator looks very promising since it’s kind of a lazy bridge between coroutines and ranges.

Rohansi 3 days ago | parent | prev | next [-]

`await` is only logically blocking. Internally the code in an async function is split up between each `await` so that each fragment can be called separately. They are cooperatively scheduled so `await` is sugar for 1) ending a fragment, 2) registering a new fragment to run when X completes, and 3) yielding control back to the scheduler. None of this internal behavior is present for non-async functions - in C# they run directly on bare threads like C++.

Go's goroutines are comparable to async/await but everything is transparent. In that case it's managed by the runtime instead of a bit of syntactic sugar + libraries.

neonsunset 2 days ago | parent [-]

Goroutines are also more limited from UX perspective because each goroutine is a true green thread with its own virtual stack. This makes spawning goroutines much more expensive (asynchronously yielding .NET tasks start at just about 100B of allocated memory), goroutines are also very difficult to compose and they cannot yield a value requiring you to emulate task/promise-like behavior by hand by passing it over a channel or writing it to a specific location, which is more expensive.

akira2501 3 days ago | parent | prev | next [-]

> Why can't I await in a normal function?

You can. promise.then(callback). If you want the rest of your logic to be "blocking" then the rest of it goes in the callback. the 'then' method itself returns a promise, so you can return that from a non async function, if you like.

> why can't I launch multiple async functions, then await on each of them, in a non-async function that does not return a promise?

Typically? Exception handling semantics. See the difference between Promise.race, Promise.all and Promise.allSettled.

mr_coleman 6 days ago | parent | prev | next [-]

In C# you can do a collection of Task<T>, start them and then do a Task.WaitAll() on the collection. For example a batch of web requests at the same time and then collect the results once everything is done. I'm not sure how it's done in other languages but I imagine there's something similar.

avandekleut 7 days ago | parent | prev | next [-]

At least in node, its because the runtime is an event loop.

MatmaRex 3 days ago | parent | prev | next [-]

You can await in a normal function in better languages, just not in JavaScript.

egeozcan 3 days ago | parent [-]

> Why can't I await in a normal function? await sounds blocking

> You can await in a normal function in better languages, just not in JavaScript.

Await, per common definition, makes you wait asynchronously for a Task/Promise. How on earth are you going to "await" for a Promise which also runs on the same thread on a synchronous function? That function needs to be psuedo-async as in "return myPromise.then(() => { /* all fn code here */ }), or you need to use threads, which brings us to the second point...

With the closest thing to threads (workers) in JavaScript and using SharedArrayBuffer and a simple while loop, perhaps (didn't think too much on it), you can implement the same thing with a user defined Promise alternative but then why would you want to block the main thread which usually has GUI/Web-Server code?

MatmaRex 2 days ago | parent [-]

It seemed to me that the previous poster wanted a way to wait for the result of a promise (in a blocking manner), and I meant that this is available in other languages. You're right that it is not usually spelled "await".

chrisweekly 3 days ago | parent | prev [-]

yeald -> yield