Skip to content

Instantly share code, notes, and snippets.

@bakkot
Created December 5, 2021 20:25
Show Gist options
  • Save bakkot/3d0f81233fc00b508ae5f247b1458823 to your computer and use it in GitHub Desktop.
Save bakkot/3d0f81233fc00b508ae5f247b1458823 to your computer and use it in GitHub Desktop.

Optionally async functions

Or: two-color functions.

The problem: async is infectious.

There's a famous essay about this, called What Color is your Function, which I recommend reading.

For example, say you're writing a bundler. It needs to fetch resources, but is agnostic to how those resources are fetched, so it takes a readResource function from its caller. In some contexts the caller might have those resources synchronously available; in others not. That is, caller might have a synchronous readResource which returns a resource immediately, or an async readResources which returns a promise for a resource, which will need to be unwrapped with await.

You can't easily write single function which handles both cases. You either need to write two functions or, more likely, just say your function is always async, even when called in a way which would require no async behavior.

And choosing to make your function async make it unsuitable for use in any synchronous code. So that code is going to need to become async too in order to use your function, which is at best annoying and at worst impossible.

This is bad.

A potential partial solution

A new syntax for defining functions which are optionally asynchronous, depending on how they're called. Instead of await, these contain await?, which behaves exactly like await when the function is called async-style, and is a no-op when called sync-style.

In that way, you can write a single function literal which is usable in both sync and async contexts (although it would define two functions).

E.g.:

async? function bundle(entrypoint, readResource) {
  let dependencies = parseForDependencies(entrypoint);
  for (let dependency of dependencies) {
    let result = await? readResource(dependency);
  }
  // etc
}

// in synchronous contexts:
let result = bundle.sync(entrypoint, fs.readFileSync); // readFileSync synchronously returns a resource
console.log(result); // not a Promise

// in async contexts:
let result = bundle.async(entrypoint, fetch); // fetch returns a promise for a resoruce
console.log(result); // a Promise

Some details

  • There should be a way for the function to switch on whether it is called as async, e.g. a function.async meta-property: let read = function.async ? fs.promises.readFile : fs.readFileSync.

  • for await? (x of y) would work similarly: exactly like for-of when called as sync, exactly like for-await when called as async.

  • Assume the obvious extension to async? function* generators.

  • Result of the function definition is a { sync, async } pair, not itself a callable. Both properties are regular functions.

  • There is no reflection involved - whether the function is async depends only on how it's called, not what values are passed to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment