8
$\begingroup$

Some languages, such as JS and Rust, make a distinction between normal/synchronous functions and async ones. An async function is typically syntax sugar for one which returns a future/promise, which like the name implies, will (theoretically) have a value at some point in the future. In order to get a value from this promise without using a callback function, one can await it. An await can only be used in an async function, and it will "block" (typically allowing other things to run in the thread) until the promise returns.

Both JS and Rust use an async keyword in front of the function keyword to make a function asynchronous. While JS uses a prefix await operator, Rust uses a postfix .await which looks like a property access. I've seen complains about both syntaxes, many of which I agree with.

What are some different options for async/await syntax, and what are their pros and cons?

$\endgroup$
2
  • $\begingroup$ The answers so far focus on await, but I’m curious about async as well — JS async function foo() vs Swift func foo() async, for example. $\endgroup$
    – Bbrk24
    May 19, 2023 at 17:08
  • $\begingroup$ @Bbrk24 Ooh I've never seen postfix async before, it's worth making that an answer I think $\endgroup$ May 19, 2023 at 21:45

8 Answers 8

9
$\begingroup$

Prefix await operator

Used by:

  • JavaScript
  • Python

Pros:

(None)

Cons:

  • Can be ambiguous with an identifier (e.g., since JS added this in after the fact, the error message for await in a non-async function is incredibly cryptic to maintain backwards compatibility)
  • Ambiguous what it's applying to, requiring parentheses (is await y().z() awaiting y() or .z()?)
  • Reads out of order (await (await fetch(...)).body())

Postfix .await property/operator

Used by:

  • Rust

Pros:

  • Reads in the same order as methods (.post().await.to_string())
  • Distinct from other methods due to lack of ()

Cons:

  • Looks like a struct/object property instead of an operator

Postfix .await() method/operator

Used by:

(Nothing?)

Pros:

  • Reads in the same order as methods

Cons:

  • Looks like an ordinary method, rather than one which has a special meaning

Implied await

Implied await would be a system where a promise/future returned by a function would, by default, be awaited, with some sort of operator being used to force it to return a promise. This operator could even work on non-async functions, meaning that the interface with a function stays identical regardless of whether or not it's async, removing the need to go through and add await to every instance of a function call.

Used by:

  • Kotlin (although async is called suspend there)

Pros:

  • Saves clutter
  • Interface with a function is the same regardless of whether it's async

Cons:

  • Could cause confusion?
$\endgroup$
9
  • 1
    $\begingroup$ Swift uses prefix await, but avoids the parentheses issue by letting you “factor it out”: Instead of await (await x()).y(), you can just say await x().y(). $\endgroup$
    – Bbrk24
    May 16, 2023 at 20:13
  • $\begingroup$ @Bbrk24 Ooh, interesting. Maybe post that as an answer? $\endgroup$ May 16, 2023 at 20:19
  • $\begingroup$ Just added it to my existing answer. $\endgroup$
    – Bbrk24
    May 16, 2023 at 20:27
  • $\begingroup$ “Looks like an ordinary method, rather than one which has a special meaning” isn’t the the whole point? Many methods are special in many languages $\endgroup$
    – Seggan
    May 17, 2023 at 18:39
  • 1
    $\begingroup$ I would put a pro in "prefix await operator": since it's used by many big languages it's more common than the alternatives. Implicit await can be ambiguous (do I want to pass the promise or the value? We could do based on type, but what if it's a generic?) $\endgroup$
    – tarzh
    May 19, 2023 at 23:59
8
$\begingroup$

To add to rssh's answer, the need for special syntax for async/await derives from a lack of a general effect system.
Effect-aware languages such as Koka encode effects in function types and have async be an user-defined effect.
In Koka, such a function may be written

effect async
    fun await(f: (a -> io ()) -> io ()): a

fun sleep(d: duration): async ()
    await fn(cb) {
        set-timeout(cb, d.milliseconds)
    }

fun f(): async int
    sleep(2.ms)
    42

See Structured Asynchrony with Algebraic Effects, Daan Leijen, 2017, and Koka's async/await implementation.

Other languages in this category include Eff, Effekt, Ante, Granule, and Unison, among others.

As far as syntax go, some of the ones used by those languages are

  • a -> b c as in int -> async string
  • a -{b}> c as in int -{async}> string
  • a -> '{a} c as in int -> '{async} string
$\endgroup$
4
$\begingroup$

Distinction between asynchronous functions and Tasks/Futures/Promises

This is what Swift does. Asynchronous functions are not distinguished by returning a Task, as in many other languages. Rather, async is an intrinsic part of the function, and you can even overload on it — foo() and await foo() could do different things, and both or neither of these could return Tasks.

A major advantage to this is that it allows asynchronicity to apply at the level of the entire expression, rather than just a single method call. Instead of having to say await (await x()).y(), one can just write await x().y().

The only way to kick off asynchronous work from synchronous code is to wrap it in a Task { }, equivalent to C# Task.Run.

$\endgroup$
4
$\begingroup$

In addition to previous answers:

  1. Automatic coloring via implicit conversions. I.e., when we have an implicit conversion from F[X] to X, which is enabled when the result of the async function is used as X and this is 'safe'. (Safety analysis depends on the type of F[_])

details: one of the modes, supported by dotty-cps-async (user-level implementation of async/await for Scala) Not very popular - people are afraid to enable it.

  1. All is async by default (Ozz, variant of C# used in Singularity)

  2. It's more about effects: representing async computation as a context function which returns result in the direct form. (i.e. DirectContext[F] => A. instead F[A])

Variants were discussed on the Scala contributors forum. Exists few projects which explore this direction.

  1. Computation expressions: I.e., after async, we have expression language enriched by special constructions (let! binding, await, ... etc.) F#,

  2. Using monadic effects DSL instead async functions (close to 4). Haskell do-notation

$\endgroup$
4
$\begingroup$

Postfix async

Everything else so far talks about await, but what about async? Most languages I'm familiar with put it at the front, e.g:

async function foo() {
  // ...
}
async Task Foo()
{
  // ...
}

However, Swift puts it at the end:

func foo() async {
  // ...
}

My understanding is that this is to mirror the syntax of throws/try:

func foo() async {}
func bar() throws {}
func baz() async throws {}

await foo()
try bar()
try await baz()
$\endgroup$
3
$\begingroup$

Deferred await

A system can be constructed such that if the actual value of the future/promise is needed (e.g. for addition or printing, but not for purely structural operations), there's an implied await. An explicit await is probably needed too, but will be much less frequent.

$\endgroup$
3
$\begingroup$

A language sketch I once made, had await-by-default. That is, the expression f(x) was equivalent to await f(x) in languages like Python and JavaScript. In order to not await, there was special syntax, f!(x) which would call f but not await its return value.

A major downside for this is that this requires the entire language to be built around async/await semantics. Effectively, every function is an async function. Presumably, this would be hard to implement efficiently.

$\endgroup$
2
$\begingroup$

No special syntax

In this case, async/await is just a normal function call.

This is an option for languages with coroutines. This includes Go and Lua, but may also work for any language with generators (e.g. C#, Rust, and Python).

An example of such an implementation is at https://github.com/ms-jpq/lua-async-await

local a = require "async", async = a.sync, await = a.wait, await_all = a.wait_all

local do_thing = async(function (val)
  local o = await(async_func())
  return o + val
end)

local main = async(function ()
  local thing = await(do_thing())

  local x = await(async_func())
  local y, z = await_all{async_func(), async_func()}
end)

main()
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .