Turning functions with a callback async

Recently, I was looking into some browser extension API's and noticed that it's very heavy on callbacks. It became apparent that people were aware of this, since in a newer version, they've made many of the methods provided async. Essentially transforming methods from

some.api.method(foo, bar, result => {
    // do something
});

to

await result = some.api.method(foo, bar);
// do something

This is nice because it keeps things simple and readable, and things don't become a pyramid of indentation.

I realized this pattern is not just applicable to extension API's, but to any function with a callback. This gave me an idea; what if we had some syntax to create this without the need for the method itself to be async?

For example, instead of this

function asyncAPIMethod(foo, bar){
    return new Promise(resolve => some.api.method(foo, bar, resolve));
}

what if we could just write this:

await some.api.method(foo, bar, %resolve);

(okay, maybe not write %resolve, but this is just a demonstration of the idea).
This would open up quite some doors:

// wait until the document loads
await document.addEventListener('DOMContentLoaded', %resolve);
// wait 1 second
await setTimeout(%resolve, 1000);
// wait until my favorite image loads
await image.addEventListener('load', %resolve);

This would also mean we don't have to force an async API onto users, but users get to choose for themselves whether they want to await our callback or if they want things to be synchronous.

Thoughts?

2 Likes

You can already do await new Promise(resolve => some.api.method(foo, bar, resolve)). That seems simple and clear to me, such that it doesn't really make sense to try to add new syntax to address this use case. Do you disagree?

1 Like

It also gets tricky if you're dealing with node-style callbacks, where the error is passed in as the first argument. There wouldn't be an easy way to translate those into promise rejections.

It seems like a nice idea, but it also seems hard to make it flexible enough to handle different callback styles.

@bakkot I disagree somewhat - yes, I can wrap it in a promise just fine, but that's a bit verbose and generally needs to be its own separate function. On the other hand, you may be right in that the use case is not broad enough to justify this idea.

@theScottyJam well, we could potentially return the aruments as an array, so one could do

const [error, data] = await fetchData(%resolve);

this would however make single-argument callbacks uglier because you'd expect it to return a single value, not an array of a single value.

I'm not sure it worth adding a new syntax, we can already promisify callbacks using wrapper functions, and the result looks not that bad:

const promisify = function (funct) {
  return (...args) => new Promise((res) => funct(...args, res));
}

// Using it
const res = await promisify(some.api.method)(foo, bar);

For Node callbacks, we can create a variation:

promisify.node = function (funct) {
  return (...args) => new Promise((res, rej) => funct(...args, (err, data) => {
    if (err) rej();
    res(data);
  }));
}

// Using it
try {
  const res = await promisify.node(some.node.api.method)(foo, bar);
} catch (err) {
  console.error(err);
}

There is also the util.promisify function of Node achieving something similar for Node callbacks.

A related (stage 1, may not happen, can change) idea is GitHub - tc39/proposal-partial-application: Proposal to add partial application to ECMAScript

Which for the examples in this thread could look something like:

// generic helper
const P = fn => new Promise(fn)

const v = await P(document.addEventListener('DOMContentLoaded', ?))
3 Likes