Monday, April 3, 2017

To the promised land, with async/await and Node 7

So you want to synchronously call asynchronous commands?   What?!?!?   But there are times when you do need to, such as stringing together dependent calls.   Request-Promise did make this a bit cleaner, but the code still gets messy.  This is where Node 7 and async/await come to save the day.

First, say we have 2 calls to request data, then 2 calls that need that data and need to be called in order.   I was able to solve this problem with request promise, but it is not ideal.   First, the very straightforward RP solution.

rp(restCall1)
    .then(function(response1) {
        // processCall1        })
    .catch(function(error) {
        // handle error from Call1    })

This is fine for a single call, but now make 2 calls, with the 2nd dependent on the 1st call.
rp(restCall1)
    .then(function(response1) {
        // processCall1        rp(restCall2)
            .then(function(response2) {
                // processCall2            })
            .catch(function(error2) {

            })
    })
    .catch(function(error1) {
        // handle error from Call1    })

That's managable, but getting messy.   Make 3 or 4 calls and it becomes an indentation nightmare.  You can unwind the nightmare a bit by using callback functions instead of putting them inline.

function processCall1(response1) {
    rp(restCall2).then(processCall2).catch(processError)
}

function processCall2(response2) {
    rp(restCall3).then(processCall3).catch(processError)
}

function processCall3(response2) {
    rp(restCall4).then(processCall4).catch(processError)
}

function processCall4(response4) {
    // Do final processing.}
    
function processError(error) {
    
}

rp(restCall1).then(processCall1).catch(processError1);

But now to show the process, you need to document carefully, name functions carefully and still when you go back and look at it, you have to really trace through each call to figure out what happens and in what order.   It's confusing and takes time if you wrote the code.   It's even harder if someone else is looking at the code.

Enter async and await.  You can wait for any promise.   You just need to make sure it's returned from the method you are calling.    The rp call returns the actual promise with rp().promise().   So now the code gets much simpler and more readable.

async function doComplicatedProcess(input) {
    try {
        let response1 = await processCall1(input);
        let response2 = await processCall2(response1);
        let response3 = await processCall3(response2);
        let response4 = await processCall4(response3);
    } catch (error) {
        console.log(error);
    }
}

async function processCall1(input) {
    return rp(restCall1).promise();
}

async function processCall2(response1) {
    return rp(restCall2).promise();
}

async function processCall3(response2) {
    return rp(restCall3).promise();
}

async function processCall4(response3) {
    return rp(restCall4).promise();
}
Now it reads like it is supposed to.   Do call 1, then 2, then 3, then 4.   Simple and easy.   Any function that returns a promise you can wait for with await.   Easier to understand also means easier to maintain.

Now what if process1 and process2 aren't needed until process4?  And they do take some time to execute?   Now we have the power to make our call even faster.
async function doComplicatedProcess(input) {
    try {
        let promise1 =  processCall1(input);
        let promise2 =  processCall2(input);
        let response3 = await processCall3(input);
        let [response1, response2] = await Promise.all([promise1, promise2]);
        let response4 = await processCall4(response1, response2);
    } catch (error) {
        console.log(error);
    }
}

async function processCall1(input) {
    return rp(restCall1).promise();
}

async function processCall2(input) {
    return rp(restCall2).promise();
}

async function processCall3(input) {
    return rp(restCall3).promise();
}

async function processCall4(response1, response2) {
    return rp(restCall4).promise();
}

Now call 1 and call 2 are processing while call 3 is being processed.   Then the await Promise.all makes sure call1 and call2 are done before going on to call 4.   It's a beautiful thing.   I reduced the average call time by 30% in my app which improved customer response time.   It also made it so that co-workers understand the code and what it's doing by looking, rather than having to trace through.

No comments:

Post a Comment