Use "await" when you really need the results

We have seen two or more async function calls in a sequence during our javascript years. While this works fast in local enviroments, they extend runtimes in production environments where these calls bring results from cloud services or remote locations.

Consider the following scenario: You want to fetch some results from a remote location, but also want to do some "other work" in the meantime, use the return values of both these functions before your program exits. That is, run them concurrently and use the results when ready.

Instead of putting await during each call, we will put await when we really need the results and finish our "other work" in the meantime. For the sake of demonstration, we will make two async function and always evaluate "other work" with await to simulate synchronous execution.

Try copying the following code and running it on your browser console. How much time do you think each function will take and why?

// returns 3 after 2s of remote work
async function remoteWork(){
    return new Promise(resolve => setTimeout(()=>{resolve(3)}, 2000));
}

// returns 5 after 1s of local work
// we will run this with await to simulate synchronous execution
async function localWork(){
    return new Promise(resolve => setTimeout(()=>{resolve(5)}, 1000));
}

async function earlyAwait(){
    const x = await remoteWork()
    const y = await localWork()

    return x + y
}

async function laterAwait(){
    // async function return a pending promise
    const p = remoteWork()
    const y = await localWork()

    const x = await p
    return x + y
}

// helper function to time execution
async function timeIt(f){
    const start = Date.now()
    const returnValue = await f()
    console.log('time taken for', f.name, 'is', Date.now() - start, 'ms')
    console.log('return value:', returnValue)
}

await timeIt(earlyAwait)
await timeIt(laterAwait)

Output:
time taken for earlyAwait is 3017 ms
return value: 8
time taken for laterAwait is 2011 ms
return value: 8

Yes, your intuitions are correct, earlyAwait() will take about 3 seconds to finish whereas laterAwait() will take about 2 seconds. And you know why too. For earlyAwait(), the program is waiting for the remote work to finish before starting the local work. On the other hand, in laterAwait(), the program only awaits for remote work to finish after finishing local work. This is possible because async functions return pending promises before they are resolved or rejected and we can put await on pending promises.

With that said, some programs may depend on a lot of remote works and writing await for each is just unacceptable. So we can use Promise.all and Promise.allSettled. Both of these take a list of pending promises and return a pending promise that is resolved when the list of pending promises are resolved. For example, Promise.all will return a reject object when any of promises in its list returns a reject object whereas Promise.allSettled will wait for all promises to either resolve or reject before resolving.

This works well for remote work and things that are evaluated asynchronously like reading from disk. But we must be attentive of what resource each function uses. No doubt, both functions use CPU time but the remote work (whose implementation I skipped and instead put a waiting code) will use network resources. At some point, if we use too much of any resource, our program will slow down as well as other processes. For example, reading multiple files from disk concurrently will take more time because of I/O limitations or sending a lot of network requests can cause bandwidth saturation. On the other hand, fetching from databases are alright because they are designed to handle multiple connections and requests via concurrency control.

Anyways, here's an example with Promise.all, try it on your browser console and the result printed should be 20 after approximately 2 seconds.

async function awaitLaterWithPromiseAll(){
    // loop over a dummy array and get a list containing 5 promises
    const xPromises = [1,2,3,4,5].map(x => remoteWork())
    const y = await localWork()

    const xList = await Promise.all(xPromises)
    const xSum = xList.reduce((sum, value) => sum + value, 0)

    return xSum + y
}

timeIt(awaitLaterWithPromiseAll)

Output:
time taken for awaitLaterWithPromiseAll is 2016 ms
return value: 20