Ask HN: Why can't await be used from non-async functions?

36 points by endorphine ↗ HN
I'm trying to wrap my head around async/await and how it works under the hood, be it JavaScript, C# or others.

From what I understand, in JavaScript at least, putting `await foo()` inside an async function, splits the calling function in two, with the 2nd half being converted to a callback. (Pretty sure this is full of errors so please correct me where I'm wrong)

Why can't non-async functions use await()?

I've also read that await() basically preempts the currently running function. How does this work?

Update: I'm re-reading http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y... and I think the answer lies somewhere in this paragraph, but I can't wrap my head around it yet:

> The fundamental problem is “How do you pick up where you left off when an operation completes”? You’ve built up some big callstack and then you call some IO operation. For performance, that operation uses the operating system’s underlying asynchronous API. You cannot wait for it to complete because it won’t. You have to return all the way back to your language’s event loop and give the OS some time to spin before it will be done. Once operation completes, you need to resume what you were doing. The usual way a language “remembers where it is” is the callstack. That tracks all of the functions that are currently being invoked and where the instruction pointer is in each one. But to do async IO, you have to unwind and discard the entire C callstack. Kind of a Catch-22. You can do super fast IO, you just can’t do anything with the result! Every language that has async IO in its core—or in the case of JS, the browser’s event loop—copes with this in some way. [...]

I don't get the "You cannot wait for it to complete because it won’t." or the "But to do async IO, you have to unwind and discard the entire C callstack" parts.

I'm also using these resources, they help but I'm not there yet:

- https://stackoverflow.com/questions/47227550/using-await-ins...

- https://www.youtube.com/watch?v=KmMU5Y_r0Uk

90 comments

[ 3.1 ms ] story [ 150 ms ] thread
Non-async functions don't have the features that enable them to be split into multiple functions that represent all the parts between the await()'s. That functionality is expensive in terms of time and space so that's why all functions aren't async.

When you call await the function doesn't necessarily have to be preempted (maybe the async function you called already returned) but if the async function hasn't returned then it has to be.

In most systems like that there is a scheduler that keeps a list of things that are being awaited on and keeps track of which ones are ready to return. When you call await on one function the scheduler looks at that list and chooses an await that is ready to continue and executes it.

What are you "awaiting" in a sync function? There's nothing to wait for, at the very least it may be you are looking for similar under-the-hood functionality of splitting functions, but even then you would name it something else. Regular functions just execute as expected, and splitting them would have no real effect on overall performance.
I'm awaiting for the promise of the function I'm calling to resolve, similar to what I do in an async function. Why is there nothing to wait for?

Actually I don't understand why the `async` keyword is needed at all.

If you're waiting, it's not synchronous by definition. It's already an asynchronous function.
This is kind of turning the classical definitions on their head.

Synchronous I/O functions don't rerurn until the I/O is complete (or complete enough, anyway). The calling function effectively waits for completion.

Asynchronous I/O functions return right away, but completion is signaled elsewhere. The calling function does not have to wait (but it can choose to wait for completion if necessary). Asynchronous messaging may not even provide a completion signal, if you want to know it got there, the other side needs to send an asynchronous message back to confirm.

Await/async is confusing enough without redefining terms to mean their opposites.

Yes, exactly what I was thinking. I think that I was confused in the first place because `await` I thought meant "block until this function returns". Whereas it really means: "don't wait, swap me out".
Yeah, I think a lot of that is the interface isn't great/the abstraction is leaky and you need to know a lot about the details. Because implementations of async/await seem to usually rely on some sort of thread sharing, await also implies yield, and there's often no or limited preemption. Of course, in Javascript, you don't usually have multiple threads at all (unless you fit into one of the special exemptions that allow multiple threads), so your options are pretty limited.
If you want your function to block until the promise resolves, then the function calling it also has to block, and the function calling that has to block, and so forth.

At the top of the chain, this ultimately blocks the entire event loop (Javascript semantics are generally not concurrent), so no UI/network events can be processed until that promise resolves and the page/server is left non-responsive.

(And that's assuming you can somehow define clear semantics to run any Javascript code involved in resolving the promise; otherwise, you're deadlocked!)

If you have one thread, and it's just in a loop checking and rechecking on a promise to resolve, you will deadlock, because nothing is actually running the async function.

Await actually pauses your function and allows the async runtime to work on other stuff, such as completing the function you just awaited on.

At least in python it is needed so that the interpreter can issue very cryptic syntax errors.

No, I'm not bitter.

> Actually I don't understand why the `async` keyword is needed at all.

Indeed:

    $ raku -e'sub non-async { await start { say "hello world" } }; non-async'
    hello world
Async functions are conceptually quite similar to ES6 generator functions. In an ES6 generator every `yield` expression is a point where the function can be paused and later resumed. With an async function it is an `await`. The difference between a generator and an async function is that whereas the caller manually resumes generators by calling (`next`) on the generator, async functions are automatically resumed when they yield a promise that is resolved.

Making a function support being paused and resumed requires changes in how the function is run and the data that is maintained while it is executing. In addition to behaving differently than sync functions when they execute, the results are also handled differently. Async functions always return a promise - whether they `return` a literal value, return another promise or throw an exception.

Due to these differences, it makes sense that the special nature of async functions and generators must be declared up-front, with the `async` keyword or `*` for generators. It would be possible to design the language such that the function type was determined by looking at whether it contains `yield` or `await` keywords. Python does this with generator functions. However this makes an important aspect of behavior less explicit.

To expand on the link between generators and async functions, the way that async functions were made to run in old browsers that didn't support them by tools like Babel and TypeScript was to rewrite the async function in two stages:

1. Rewrite the async function to be a generator function with a wrapper that calls `next` on the generator whenever `await`-ed Promises resolve

2. Rewrite the generator function to be a big switch statement, together with a wrapper function that drives execution.

You can play around with generators and async functions in the TypeScript playground, with TSConfig set to target ES5, to see how this works: https://www.typescriptlang.org/play?target=1#code/GYVwdgxgLg...

> Making a function support being paused and resumed requires changes in how the function is run and the data that is maintained while it is executing. In addition to behaving differently than sync functions when they execute, the results are also handled differently.

Can you expand on what the actual differences are?

That starts to make sense to me: normal functions/subroutines will execute from start to finish, while these async functions (coroutines?) can be paused right in the middle of its execution. This is something fundamentally different then, which could explain why we need the `async` keyword, because those "asynchronous" functions are special.

I'd be interested to know the actual differences.

Is it true that async/await is syntactic sugar over Promises, and that Promises are sugar over callbacks?

If that's true, does the "function coloring" problem apply to Promise-based code and callback-based code too? (Talking about JavaScript here)

Yes, function coloring applies to them too. The function coloring problem is semantic, not syntactic (with the possible exception of rule 4 "Red functions are more painful to call"), so no amount of adding or removing syntactic sugar will affect it.
Pretty much... all you need to provide to `await` is an object with a 'thenable' function. Even this works:

  (async () => {
    const promise = {
      then (resolve) {
        resolve('World');
      }
    };
    console.log('Hello', await promise);
  })()
If you could await in a regular function then that function would need to return a promise. But it would be confusing if adding an await keyword to the middle of a function body implicitly changed it into returning a promise. Thus we enforce that you have to use the async keyword on the function in order to enable this functionality.
> If you could await in a regular function then that function would need to return a promise.

Why would it have to return a promise?

I understand where you're coming from. But, how would this be any more confusing than the fact that using yield instead of return in Python turns a function into a generator? That just seems like a pretty thin justification for not allowing await without async.
I mean JavaScript makes the same decision here as well. To use yield you must define a generator using "function*" rather than a function using "function" exactly because it's confusing. Is it really so hard to to type `async`?
That's not a justification. Maybe JS used the same flawed reasoning?
just remember it is just sugar:

const a = async () => "hello world"

is the same as

const b = () => Promise.resolve("hello world")

(comment deleted)
Async/await requires a run loop. Async functions are special functions that can suspend execution and jump back to the run loop, and at some later point the run loop continues them.

Normal functions can't do that. If they wait for something they block, they can't give control back to the run loop.

So async functions are special functions that have the capability to be _paused_ until another asynchronous operation finishes (i.e. the thing we're `await`ing upon).

And in a single-threaded environment, we _want_ this capability so that the loop is able to continue working despite the fact that the thing we're `await`ing for hasn't complete yet.

It's conceptually a different thing to "await" and to "stop execution till the thing I am waiting for happens"

In the first case I am telling an executor what it has to do (it has to execute the thing then continue inside my function)

In the second case I am myself blocked into a state that will be unblocked when the thing returns.

That is in the first case my function must be of a type that can be stopped and continued and I need to save its state somewhere.

In the second case I don't need the ability to stop and continue my function.

That makes for two kinds of functions at some level. Low level languages will expose that. High level languages will hide it.

But there is a growing request for functions that can be both. I think that zig has them

the requirements of the second case are a subset of the first case, so then it should be theoretically possible to await inside non async functions. Seems to me that JS simply doesn't implement this ability, but it could if it wanted to
> That is in the first case my function must be of a type that can be stopped and continued and I need to save its state somewhere.

And that state is saved in the heap, I presume? Can you provide a concrete example perhaps?

I get that the state in case of green threads (Goroutines for example) is saved in the call stack itself. Where all this is saved in async/await?

The typical implementation of async/await need to store (the equivalent of) a single activation frame (as opposed to a stack of activation frames). This is normally stored separately on the heap, but it can be embedded in some other (heap allocated object) or even stored directly on the stack of some thread, depending on language, usage patterns and optimization capabilites.
> I'm trying to wrap my head around async/await and how it works under the hood, be it JavaScript, C# or others.

Careful, it doesn't work the same way in every single language that has these keywords.

> Why can't non-async functions use await()?

Because in Javascript at least, it only makes sense to await a promise, nothing more. A function marked as async will always return a promise. You can't await on anything that is not a promise.

This isn't _exactly_ true. JS will await on anything that is Promiselike (an object with a `then` function that accepts a callback). See the TS definition of Promiselike.
JS will await on literals too, it just immediately resolves to the value of the literal.

I use this all the time in async functions with code branches, some of which await and some return an immediate value.

> A function marked as async will always return a promise.

I understand that async functions return promises, but why can't you await for a promise from a non-async function? For me it would seem logical since by awaiting you essentially turn a promise into a concrete result which you can then use in your non-async code.

(comment deleted)
I think this is technically incorrect.

It's perfectly valid to await on a literal:

  > async function test() {

    let a = "yep"; 
    await a; 
    console.log(a); 

  }

  > test()

  > yep
when you use `await a`, a is wrapped in a promise. Try returning await a - you get a promise and not the literal. In your example you don't use the promise, and a is a literal.

> async function test() {

  let a = "yep"; 
  return await a; 
}

> let b = test()

> console.log(b)

> Promise { 'yep', [Symbol(async_id_symbol)]: 1215, [Symbol(trigger_async_id_symbol)]: 5, [Symbol(destroyed)]: { destroyed: false } }

The promise you create with `await a` resolves immediately (equivalent to Promise.resolve(a)), but it's still a promise.

(My academic background is engineering, not computer science, so I'm quite possibly wrong about all of this)

I think what OP is assuming is a multi-threaded or multi-process environment, where the calling function can just block whatever execution context it's running in and wait until the async function returns.

The problem is that many environments are effectively non-multi-threaded, especially the (usually single) thread/queue/process that draws the UI and responds to user input. So if you block the UI thread, your whole app (at least from the standpoint of the user) stops responding.

Still, this should work in principle, but threads are more costly in terms of memory and context switching time than continuations, so it makes sense to allow the thread to continue to handle other tasks while waiting for e.g. I/O to complete.

This is what the author of the classic https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... settles on as the best way of handling async tasks, but I recall there being some pushback on that here on HN.

That's true. I come from the threaded runtimes where we can just block the thread because there's always another thread that can take over. I think that if I try to approach this from a perspective of a single-threaded environment, I might get closer to grasping it.

> The problem is that many environments are effectively non-multi-threaded, especially the (usually single) thread/queue/process that draws the UI and responds to user input

Can you elaborate on why "especially" UI threads/queues/processes are "usually single"?

> Can you elaborate on why "especially" UI threads/queues/processes are "usually single"?

I don't think there's a particularly insightful answer to this question here, it's just that UI frameworks are almost universally written with the assumption that they are only used from one thread. UI frameworks can also use functionality spread across multiple components/libraries/systems, and making an UI kit thread-safe would likely require a lot of effort for dubious benefit (user input normally has to be processed strictly in order because clicking on a button and then pressing "enter" is different from pressing enter and then clicking on a button).

There are some specific scenarios where multiple threads are safe in a UI. For example, it's relatively common to be able to pass off an OpenGL context to another thread... so you can do OpenGL rendering in a thread separate from the main UI thread, if you like. Some UI frameworks specifically support this use case, e.g., certain methods on the OpenGL widgets are described as thread-safe. Individual OpenGL contexts are also not thread-safe and must be used from a single thread at a time (and they usually involve some thread-local context).

The typical way you make a responsive UI is by doing only UI work in the UI thread, and passing off all long-running computations to background threads.

Well, I'd call that an insightful answer! Thanks!
Since you mentioned C# and most replies so far are about Javascript -

In C#, you can't use the "await" keyword, but you can use the result of a Task<T> in a non-async function with ".Result" or ".GetAwaiter().GetResult()". https://stackoverflow.com/a/47648318/5107208

Note that it's recommend that you avoid using these because they carry a risk of deadlock. Unless something has changed recently .Net doesn't have a 100% safe way to synchronously call an async method from a non-async method.
I mean, depends on what you mean by "100% safe". You can synchronously wait from a thread that's not in the thread pool executing the tasks. Obviously that's not sufficient for safety, but it should be illustrative that safety is possible. There are also some issues with using .Wait or .Result but those can be solved.
Can you elaborate on why there's a risk of a deadlock?
The very short version is that by synchronously blocking on the async method your thread may end up holding a resource (thread context info or the calling thread itself) that the async method needs in order to run its continuations. Here are a couple of more detailed explanations:

https://blog.stephencleary.com/2012/07/dont-block-on-async-c...

https://devblogs.microsoft.com/pfxteam/await-and-ui-and-dead...

Now that's pretty much the answer I was looking for, I believe. This makes things much clearer!
> I've also read that await() basically preempts the currently running function. How does this work?

Well, I would say that it's the opposite of preemption. It's cooperative multitasking. Your async task is split into two tasks, with the await in the middle. When you call await, the first task finishes. The second task starts running once the await is done.

> Why can't non-async functions use await()?

In C#, they can. You can call `.RunSynchronously()` on a `Task`. C# supports lots of different TaskSchedulers that handle this differently.

C# is almost pathologically flexible here. Other languages typically assume that there is only one async task scheduler, and it's handled by the runtime. This task scheduler may be less flexible, and there are various design tradeoffs.

Incidentally what is the performance benefit of the whole promise/callback hell compared to GOTO? I find it needlessly confusing and inhuman.
There's no performance benefit. Promises/callbacks are abstractions which live in a much higher level programming space.

Incidentally, many high level programming languages _don't_ include GOTO, because people find it needlessly confusing and inhuman.

both are terrible concepts. Languages are supposed to help humans, or else we should all write assembly.
Because the sync function does not know what to do in case the async function doesn't return immediately and awaits something else. It can't block because that would block the whole thread, possibly including UI. Spawning a new thread and blocking there would break the whole point of async and would be expensive.
So it's not that it _can't_ block while `await`ing, it's that it doesn't _want_ to block?
There's been a lot of discussion here of the technical reasons for function coloring, but the semantic reasons for it are far more interesting and important to me. I suspect that technical limitations could (and in some languages do) have workarounds, but there's real value to the system as it's set up.

When I mark a function as `async`, I'm specifying that this function will be performing some form of asynchronous I/O. Any caller of that function (or its callers' callers) will therefore also be doing I/O. Such functions may take a while to "return" and are more likely to throw an exception.

Function coloring makes these properties explicit. By forcing all callers to also be `async`, it becomes easy to see which parts of the codebase are doing (somewhat risky) I/O work and which parts will be near-instant. No one ends up surprised when a call to `getFoo()` ends up getting `foo` from the network rather than the local object.

But async doesn't imply IO in many languages, it could just be a timer or a cross thread operation. At the limit is just lazy evaluation.

Similarly the absence of async doesn't guarantee the absence of IO, the function could be doing plain old sync IO or scheduling continuations on the event loop by hand.

IMHO there isn't any justification for async in "high overhead" languages, i.e. most of the dynamic languages that are not going to spawn tens if thousands of concurrent coroutines; those languages are better served by green threads or some sort of stackful continuations (the existence of async in python is particularly baffling).

It is more of a necessary evil in high performance languages were dedicating a call stack is a comparatively large cost. Even then I strongly believe that async functions should be awaited by default and any forking should be explicit.

YMMV.

> Why can't non-async functions use await()?

To await means to wait for a Promise to resolve asynchronously. That's why async functions return a Promise instance.

  async function asyncFn() {
    const result = await fetch(url)
    const json = await result.json()
    return json
  }
  
  asyncFn().then(json => console.log('Got', json))
  console.log('Fetching result..')
Calling asyncFn() will immediately return a Promise instance. Then each await in the function waits for a Promise to resolve, before continuing to the next instruction. While that's happening, other code can keep running, like the last line in the example above.

In contrast, a non-async function is synchronous, which means it's expected to run all instructions one after the other, then return a value. No other code can run while that's happening.

If await was allowed in a non-async function, it would have to block execution of all other code until the Promise is resolved/rejected.

  function syncFn() {
    const result = await fetch(url)  // Stop the world
    const json = await result.json() // Stop the world
    return json
  }
  
  console.log('Fetching result..')
  console.log('Got', syncFn())
This is how a synchronous HTTP request works: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequ...

To avoid stopping the world, a non-async function can use Promise callbacks instead.

  function promiseFn() {
    return fetch(url)
      .then(result => result.json())
  }
  
  promiseFn().then(json => console.log('Got', json))
  console.log('Fetching result..')
Yes, but if I am ok with stopping the world, why doesn't JS allow me to do so? Why do we need to have an entirely seperate api for sync http calls when allowing awaiting inside sync functions would have done the same thing?
JS does - just wrap all your code in an async function, and then you can await within.

> async function run() {

    // call your program logic here

    await myCode()
}

> run()

But this approach would async-pollute all the sync calling functions right? if I had a call tree like

  function A(){ B()}

  function B(){ C()}

  ...

  function Y(){ await? Z()}

  async function Z(){...}
Then A-Y would need to become async. If await Z() was allowed then we could avoid this. So is there a reason why it could not be implemented this way?
Because JS is single-threaded¹ and is designed for use on the web. If you block that thread, then all event processing ceases—button clicks don't register, scroll events don't fire, etc. So all JS API's are designed to be non-blocking.

¹Not really, but close enough.

Originally, this was resolved via callbacks:

  doSomething((err, result) => {
    if (err) { ...failure code... }
    ...success code...
  }
But that got very clunky, resulting in something called "callback hell" and the "callback pyramid of doom." So promises were introduced:

  doSomething().then((result) => {
    ...success code...
  }).catch((err) => {
    ...failure code...
  });
But that's also clunky, so the async/await syntax was introduced:

  try {
    let result = await doSomething();
    ...success code...
  }
  catch (err) {
    ...failure code...
  }
And that's actually quite pleasant to use.
(comment deleted)
I haven't tried it and surely not recommended, but I think it's possible to block execution with a while loop until the promise is resolved/rejected. This will be like a "synchronous await", which defeats the purpose of having async functions.

  function syncFn() {

    let result
    let state = 'pending'

    asyncFn()
      .then(data => {
        result = data
        state = 'fulfilled'
      })
      .catch(error => {
        result = error
        state = 'rejected'
      })

    while(state==='pending') {} // Stop the world

    return result
  }

  console.log('Result', syncFn())
I don't think this will work in JavaScript because that while loop will block the event loop and prevents callback from executing. For example, the following code will be blocked when running in Node.js:

    function delayedValue(val, ms) {
        return new Promise(resolve => setTimeout(() => resolve(val), ms))
    }
    
    function syncFn() {
        let result
        let state = 'pending'
    
        delayedValue(1234, 500)
            .then(data => {
                result = data
                state = 'fulfilled'
            })
            .catch(error => {
                result = error
                state = 'rejected'
            })
        while (state == 'pending') {}
        return result
    }
    console.log('before')
    console.log(syncFn())
    console.log('after')
I'll take a stab:

In JavaScript, you only have one thread. You really don't want to block it for any length of time. IO tends to take time. So IO is always async. There's no way to turn something async into something synchronous without blocking your thread waiting.

Other languages do let you block a thread to wait for an async task to complete, JavaScript doesn't mostly because there is only one thread.

I see, so the asynchronous model that JavaScript implements is an answer to the question:

> Given a single thread, how can we do multiple things at once (slow I/O, process user events like a mouse click etc.)

And the answer to this, is that we use continuations. Is that correct?

Yep, and even in languages with multiple (OS) threads, a thread is still a resource with a stack that takes some memory, and so they might also do non-blocking IO with either with callbacks or promises or similar.
In JavaScript, "await" is syntactic sugar for promises.

  async function myFunction() {
    a();
    await b();
    let d = c();
    return d;
  }
becomes something like this:

  function myFunction() {
    return new Promise((resolve, reject) => {
      a();
      b().then(() => {
        try {
          let d = c();
          resolve(d);
        }
        catch (err) {
          reject(err);
        }
      }).catch(reject);
    });
  }
If you didn't mark `myFunction()` as async, it wouldn't set up the `new Promise()` scaffolding, and there would be no `resolve()` or `reject()` to call.

Note that you can call an async function from a non-async function. Assuming b() is async and a() and c() are not, a manual approach looks like this:

  function myFunction(callback)
    a();
    b().then(() => {
      try {
        let d = c();
        return callback(null, d);
      }
      catch (err) {
        return callback(err);
      }
    }).catch(callback);
  }
There are a few different nuances to this question.

Can it be done? sure:

   let bar = await foo();
   return bar + 1;
can be converted to:

   let resultPromise = foo();
   while (!resultPromise.isComplete()){
       sleep(10);
   }
   let bar = resultPromise.value();
   return bar + 1;
the issue is that this is terribly inefficient as you are asking every 10ms whether or not the result is ready.

One of the reasons this is so inefficient is that you have to spend time checking (which is wasteful if it isnt ready) and waiting. If you check more frequently, you're wasting CPU. If you spend more time waiting, you can waste up to that much time if the result is ready right after you go to sleep.

The alternative is to have the operating system tell you when its ready. This is done through the event loop. So this would turn the code transformation into something like the following:

  let resultPromise = foo().then(function(bar){
     return bar + 1;
  });
  
but what do you return? you have to return a promise (i.e. a callback). Therefore, all callers of this function have to be ready to handle this.
I don't know of a JavaScript implementation where that approach would work. A synchronous implementation of `sleep` can't yield control back to the main thread to handle the asynchronous stuff. You'd just end up polling indefinitely.
> can be converted to:

Not necessarily! In a typical eventloop system - like with libuv - events and readiness will be checked within the eventloop while no user code is executed. The eventloop runs something along:

    while (true) {
        waitForOsEventsOrTimers(); // Uses epoll & co
        executeAllReadyCallbacks();
    }
Your whole promise code is part of `executeAllReadyCallbacks`. Thereby any blocking there will block fetching new readiness events, and thereby the code will be stuck.

It would only work if the polling OS functions would run on a different thread - which some runtimes like .NET might actually do. However its very often avoided since its less efficient than polling events inline without synchronization, and in Javascript wouldn't work.

When you now use the c

In JS, Python and similarly built systems async is inherently coupled to an inversion of control.

When you write (Python):

    await sock.write("fooooooo")
write will, after several layers of abstractions and wrappers, look like this (on a reactor/readiness-oriented system):

    def write(self, data):
        while data:
            try:
                written = self._socket.write(data)
            except EWOULDBLOCK, EAGAIN:
                yield 'writable', self._socket
            data = data[written:]
"yield" here means to pass a bit of data up to the event loop (neé coroutine scheduler), and that event loop will watch (using select/epoll) for that socket to become writable and then hand control back.

Because this inversion of control doesn't happen unless you (usually) explicitly wrap a coroutine (e.g. asyncio.run_until_complete) with an event loop, you can't just call a coroutine. It doesn't know when to resume in these designs, and your synchronous function doesn't know how to, either.