Master async/await in Node.js

  • Mastering Async Await in node.js
  • By Tamas Kadlecsik

In this article you’ll learn how to use async functions (async/await) to simplify callbacks and Promise based Node.js applications.

Asynchronous language constructs have been around for a while in other languages, such as async/await in C#, Coroutine in Kotlin, and Goroutine in Go. With the release of Node.js 8, the long-awaited asynchronous function feature is finally here.

By the end of this tutorial, you should be able to answer the following questions:

Async /await in Node.js is spontaneous…

What are asynchronous functions in Node?

Asynchronous function declarations return AsyncFunction objects. This is similar to generators in the sense that their execution can be aborted. The only difference is that their asynchronous functions always return Promise instead of {value: any, done: Boolean} objects. In fact, asynchronous functions are very similar to what you experience from co packages.

In an asynchronous function you can await any Promise or capture the reason for its rejection.

So, if you have logic like the following that you implement with Promise:

The code in the text is formatted according to the format recommended by the translator, which does not affect the meaning of the code

function handler(req, res) { return request("https://user-handler-service") .catch((err) => { logger.error("Http error", err); error.logged = true; throw err; }) .then((response) => Mongo.findOne({ user: response.body.user })) .catch((err) => { ! error.logged && logger.error("Mongo error", err); error.logged = true; throw err; }) .then((document) => executeLogic(req, res, document)) .catch((err) => { ! error.logged && console.error(err); res.status(500).send(); }); }Copy the code

You can modify it to be more like writing synchronous code with async/await:

async function handler(req, res) {
    let response;
    try {
        response = await request("https://user-handler-service");
    } catch (err) {
        logger.error("Http error", err);
        return res.status(500).send();
    }

    let document;
    try {
        document = await Mongo.findOne({ user: response.body.user });
    } catch (err) {
        logger.error("Mongo error", err);
        return res.status(500).send();
    }

    executeLogic(document, req, res);
}
Copy the code

In older V8s, if rejected promises are not handled, they are simply quietly discarded. Now at least you get a warning from Node, so you don’t have to worry about creating a listener. However, if you can’t handle errors and leave your app in an unknown state, the recommended course of action is to crash your app:

process.on("unhandledRejection", (err) => {
    console.error(err);
    process.exit(1);
});
Copy the code

Use the pattern of asynchronous functions

There are some cases where it would be nice to be able to operate asynchronously as easily as writing synchronous programs. Using promises and callbacks to handle them requires complex schemas or references to other libraries.

Here are some examples where you need to get data asynchronously in a loop, or use if-else conditions.

Retry with exponential compensation

Implementing retry logic using promises is clumsy:

function requestWithRetry(url, retryCount) {
    if (retryCount) {
        return new Promise((resolve, reject) => {
            const timeout = Math.pow(2, retryCount);

            setTimeout(() => {
                console.log("Waiting", timeout, "ms");
                _requestWithRetry(url, retryCount)
                    .then(resolve)
                    .catch(reject);
            }, timeout);
        });
    } else {
        return _requestWithRetry(url, 0);
    }
}

function _requestWithRetry(url, retryCount) {
    return request(url, retryCount)
        .catch((err) => {
            if (err.statusCode && err.statusCode >= 500) {
                console.log("Retrying", err.message, retryCount);
                return requestWithRetry(url, ++retryCount);
            }
            throw err;
        });
}

requestWithRetry("http://localhost:3000")
    .then((res) => {
        console.log(res);
    })
    .catch(err => {
        console.error(err);
    });
Copy the code

This code looks like a headache. We could rewrite this code using async/await, which would be much simpler.

function wait(timeout) { return new Promise((resolve) => { setTimeout(() => { resolve(); }, timeout); }); } async function requestWithRetry(url) { const MAX_RETRIES = 10; for (let i = 0; i <= MAX_RETRIES; i++) { try { return await request(url); } catch (err) { const timeout = Math.pow(2, i); console.log("Waiting", timeout, "ms"); await wait(timeout); console.log("Retrying", err.message, i); }}}Copy the code

This code is more pleasing to the eye, isn’t it?

The median

This example is not as scary as the previous ones, but if you have three asynchronous functions that depend on each other in order, you may have to choose one of several unattractive solutions.

FunctionA returns a Promise, and calling functionB requires that value, and then functionC needs the value brought back from functionA and functionB’s Promise.

Method 1:.thenThe Christmas tree
function executeAsyncTask() { return functionA() .then((valueA) => { return functionB(valueA) .then((valueB) => { return  functionC(valueA, valueB); }); }); }Copy the code

In this approach, we get valueA through three layers of code and valueB from the previous Promise. We can’t flatten the Christmas tree, otherwise there won’t be closures and valueA won’t be available when we call functionC.

[Translator’s note: Some links suspected of advertising were removed in the translation process]

Option 2: Move to the upper scope
function executeAsyncTask() {
    let valueA;
    return functionA()
        .then((v) => {
            valueA = v;
            return functionB(valueA);
        })
        .then((valueB) => {
            return functionC(valueA, valueB);
        });
}
Copy the code

In the Christmas tree, we use a higher derivative scope to make valueA valid. The situation is similar here, except that we now define valueA outside of all.then, and we can assign the first Promise’s definite value to it.

This works, of course, by flattening the. Then chain while preserving the correct semantics. However, it also introduces new drawbacks, such as valueA being used elsewhere in the function. We need to use two variables — valueA and v — that are the same value.

Option 3: Unnecessary arrays
function executeAsyncTask() {
    return functionA()
        .then(valueA => {
            return Promise.all([valueA, functionB(valueA)]);
        })
        .then(([valueA, valueB]) => {
            return functionC(valueA, valueB);
        });
}
Copy the code

ValueA is placed in the array along with the Promise generated by functionB, of course, to flatter the tree. They could be completely different types, so it’s highly likely that they shouldn’t be in an array at all.

Option 4: Write a helper function
const converge = (... promises) => (... args) => { let [head, ...tail] = promises; if (tail.length) { return head(... args) .then((value) => converge(... tail)(... args.concat([value]))); } else { return head(... args); }}; functionA(2) .then((valueA) => converge(functionB, functionC)(valueA));Copy the code

You could certainly be clever and write a helper function to hide the context, but such code would be very difficult to read and might not be easy to understand for those not well versed in functional programming techniques.

useasync/awaitThere would be no problem:
async function executeAsyncTask() {
    const valueA = await functionA();
    const valueB = await functionB(valueA);
    return function3(valueA, valueB);
}
Copy the code

Process multiple parallel requests with async/await

This is similar to the previous example. Here you want to execute several asynchronous tasks at the same time and use their result values in different places. This is easy to solve with async/await:

async function executeParallelAsyncTasks() {
    const [valueA, valueB, valueC]
        = await Promise.all([
            functionA(),
            functionB(),
            functionC()
        ]);
    doSomethingWith(valueA);
    doSomethingElseWith(valueB);
    doAnotherThingWith(valueC);
}
Copy the code

As we saw in this example, we don’t need to move these values to the upper scope, nor do we need to create semantically meaningless arrays to pass them around.

Array iteration method

You can use Map, Filter, and Reduce in combination with asynchronous functions, but their behavior is not intuitive. Guess what the following script will print on the console:

  1. map
function asyncThing(value) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(value), 100);
    });
}

async function main() {
    return [1, 2, 3, 4].map(async (value) => {
        const v = await asyncThing(value);
        return v * 2;
    });
}

main()
    .then(v => console.log(v))
    .catch(err => console.error(err));
Copy the code
  1. filter
function asyncThing(value) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(value), 100);
    });
}

async function main() {
    return [1, 2, 3, 4].filter(async (value) => {
        const v = await asyncThing(value);
        return v % 2 === 0;
    });
}

main()
    .then(v => console.log(v))
    .catch(err => console.error(err));
Copy the code
  1. reduce
function asyncThing(value) { return new Promise((resolve, reject) => { setTimeout(() => resolve(value), 100); }); } async function main() { return [1, 2, 3, 4].reduce(async (acc, value) => { return await acc + await asyncThing(value);  }, Promise.resolve(0)); } main() .then(v => console.log(v)) .catch(err => console.error(err));Copy the code

The answer:

  1. [ Promise { }, Promise { }, Promise { }, Promise { } ]
  2. [1, 2, 3, 4]
  3. 10

If you record the return values during the Map iteration, you will see the array we expect: [2, 4, 6, 8]. The only problem is that each is wrapped as a Promise by AsyncFunction.

Therefore, if you want the correct value, you need to unpack the returned array with promise.all.

main()
    .then(v => Promise.all(v))
    .then(v => console.log(v))
    .catch(err => console.error(err));
Copy the code

Instead, you should have waited for the Promise to confirm the value and then mapped it:

function main() {
    return Promise.all([1, 2, 3, 4]
        .map((value) => asyncThing(value)));
}

main()
    .then(values => values.map((value) => value * 2))
    .then(v => console.log(v))
    .catch(err => console.error(err));
Copy the code

That makes it look easier, doesn’t it?

Async /await can still be useful if you need to iterate over some long-running synchronous logic in a long-running asynchronous task.

That way, as soon as you have a value, you can start counting — you don’t have to wait until all the promises have been resolved. Even though the results are still wrapped in promises, they are still much faster than sequential execution.

The filter? Obviously not…

Good, you guessed it: the return values are [false, true, false, true], but they are wrapped as promises, so all the values are retrieved from the original array. Unfortunately, to fix this error, you need to determine all values and then filter.

An implementation has been added with interest

Async function main() {const promises = [1, 2, 3, 4] .map(async value => { const v = await asyncThing(value); return { value: value, predicate: v % 2 === 0 }; }); return (await Promise.all(promises)) .filter(m => m.predicate) .map(m => m.value); }Copy the code

Reduce is fairly straightforward. But remember that you need to encapsulate the initial value with promise.resolve, and the cumulative value returned will also be encapsulated and await.

If you want to use functional programming,async/awaitProbably not for you.

. Because its intent is explicitly to use the imperative programming pattern.

To make the.then chain look purer, you can use Ramda’s pipeP and composeP functions.

Rewrite the node.js application based on the callback

Asynchronous functions return promises by default, so you can rewrite callback-based functions to use promises and wait to await. You can convert a callback-based function to a promise-based function using node.js’s util.promisify function.

Rewrite the Promise based application

Simple.then chains can be upgraded directly, so you can use async/await immediately.

function asyncTask() {
    return functionA()
        .then((valueA) => functionB(valueA))
        .then((valueB) => functionC(valueB))
        .then((valueC) => functionD(valueC))
        .catch((err) => logger.error(err));
}
Copy the code

Can be changed to

async function asyncTask() { try { const valueA = await functionA(); const valueB = await functionB(valueA); const valueC = await functionC(valueB); return await functionD(valueC); } catch (err) { logger.error(err); }}Copy the code

Rewrite node.js applications with async/await

  • If you like the classicif-elseConditions andfor/whileCycle,
  • If you agreetry-catchThe way blocks handle errors,

You will happily rewrite the service using async/await.

As we’ve seen, it can make some patterns easier to write and easier to read, so it’s certainly more appropriate than the promise.then () chain in some cases. However, if you’ve been caught up in the functional programming craze of the past few years, you might want to ignore this language feature.

So what are you all thinking? Is async/await the best thing since inventing bread slices, or is it as controversial as es2015’s class?

Are you already using async/await in production, or are you going to absolutely not touch it? Let’s discuss it in the comments below.