1

I am new to JavaScript promises and would like to know if this is the common/idiomatic way of doing sharing results asynchronously in JavaScript/Node.js.

I have a method, say calc, that computes a list of items asynchronously. I only want the result to be calculated once, and other cals either get the already calculated results or wait until the result is calculated. I've come up with the following design (a minimal example to demonstrate it).

let result: Promise<number[]>;

async function calc(): Promise<number[]> {
    if (result) {
        return result;
    }

    result = new Promise<number[]>(async (resolve, reject) => {
        await delay(5000);
        resolve([1, 2, 3, 4, 5]);
    });
    return result;
}

async function delay(time: number) {
    return new Promise<void>((resolve, reject) => setTimeout(resolve, time));
}

Is there any flaws with this design? Is the calc method always asynchronous? i.e., the results run in a different stack than the caller?

Wickoo
  • 6,745
  • 5
  • 32
  • 45

1 Answers1

2

Is the calc method always asynchronous?

Consuming the result from the promise calc returns will always be asynchronous (which is probably what you really mean). This is guaranteed by the specification: then and catch callbacks are always called asynchronously, whether the promise is already settled or not.

Note that if you're returning a promise (which you are in all code paths in calc) and not using await in the immediate body of calc (which you aren't), you don't need to declare calc as async. Doing so conceptually adds a layer of promises (which is largely harmless, but...).

Is there any flaws with this design?

A couple of minor ones. delay is, of course, just standing in for the real work that you're going to do. Using await delay(); in your async promise executor callback without a try/catch means you will miss it if the promise from delay rejects. You want to ensure you handle rejection (perhaps with a try/catch in the executor callback, or by not using await in it).

Related to that, the code is also falling prey to the Promise creation antipattern. Since delay provides a promise, here's no need for new Promise in calc. Just use the one from delay.

So calc could just look like this:

function calc(): Promise<number[]> {
    if (result) {
        return result;
    }

    return result = delay(5000).then(() => [1, 2, 3, 4, 5]);
}

Since you're explicitly handling the promise, I probably wouldn't use async on it.

Live Demo (just for fun; I've made the delay 800ms instead of 5000ms):

let result;
function calc() {
    if (result) {
        return result;
    }

    return result = delay(800).then(() => [1, 2, 3, 4, 5]);
}
function delay(time) {
  return new Promise(resolve => {
    setTimeout(resolve, time);
  });
}

console.log("A");
calc().then(() => { console.log("C"); });
console.log("B");
calc().then(() => { console.log("D"); });
setTimeout(() => {
  console.log("E");
  calc().then(() => { console.log("G"); });
  console.log("F");
}, 1000);

As you can see, it outputs A B, waits 800ms, outputs C D, waits a further ~200ms, and then outputs E F G, proving that consuming calc's promise is always asynchronous.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875