Let's say I'm writing the following function:
async function foo(axe: Axe): Promise<Sword> {
// ...
}
It is meant to be used like this:
async function bar() {
// get an axe somehow ...
const sword = await foo(axe);
// do something with the sword ...
}
So far, so good. The problem is that in order to implement foo, I want to call the following "callback-style async" function. I cannot change its signature (it's a library/module):
/* The best way to get a sword from an axe!
* Results in a null if axe is not sharp enough */
function qux(axe: Axe, callback: (sword: Sword) => void);
The best way I've found to do this is to "promisify" qux:
async function foo(axe: Axe): Promise<Sword> {
return new Promise<Sword>((resolve, reject) => {
qux(axe, (sword) => {
if (sword !== null) {
resolve(sword);
} else {
reject('Axe is not sharp enough ;(');
}
});
});
}
It works, but I was hoping I could do something more direct/readable. In some languages, you could create a promise-like object (I call it Assure
here), and then explicitly set its value elsewhere. Something like this:
async function foo(axe: Axe): Promise<Sword> {
const futureSword = new Assure<Sword>();
qux((sword) => {
if (sword !== null) {
futureSword.provide(sword);
} else {
futureSword.fail('Axe is not sharp enough ;(');
}
});
return futureSword.promise;
Is this possible in the language itself, or do I need to use a library/module, like deferred?
Update (1): extra motivation
Why would one prefer the second solution over the first? Because of callback chaining.
What if I wanted to perform multiple steps inside foo, not just call qux? If this was synchronous code, it could look like this:
function zim(sling: Sling): Rifle {
const bow = bop(sling);
const crossbow = wug(bow);
const rifle = kek(crossbow);
return rifle;
}
If these functions were async, promisify-ing would give me this:
async function zim(sling: Sling): Promise<Rifle> {
return new Promise<Rifle>((resolve, reject) => {
bop(sling, (bow) => {
wug(bow, (crossbow) => {
kek(crossbow, (rifle) => {
resolve(rifle);
});
});
});
);
}
With an Assure
, I could do this:
async function zim(sling: Sling): Promise<Rifle> {
const futureBow = new Assure<Bow>();
bop(sling, (bow) => futureBow.provide(bow));
const futureCrossbow = new Assure<Crossbow>();
wug(await futureBow, (crossbow) => futureCrossbow.provide(crossbow));
const futureRifle = new Assure<Rifle>();
kek(await futureCrossbow, (rifle) => futureRifle.provide(rifle));
return futureRifle;
}
I find this more manageable, since I don't need to keep track of the nested scopes and to worry about the order of computation. If functions take multiple arguments the difference is even larger.
Reflection
That said, I could agree that there is an elegance to the version with the nested calls, because we don't need to declare all these temporary variables.
And while writing this question I got another idea, of how I could keep in stride with the spirit of JavaScript:
function zim(sling: Sling): Rifle {
const bow = await new Promise((resolve, reject) => { bop(sling, resolve); });
const crossbow = await new Promise((resolve, reject) => { wug(bow, resolve); });
const rifle = await new Promise((resolve, reject) => { kek(crossbow, resolve); });
return rifle;
}
... which starts to look a lot like using util.promisify
from Nodejs. If only the callbacks were following the error-first convention... But at this point it seems justified to implement a naive myPromisify
which wraps promisify
and handles the type of callbacks I have.