Dima Grossman wrote How to write async await without try-catch blocks in Javascript I was very curious when I saw the headline. I know that it is possible to handle an error with async/await without a try-catch, but the processing does not work well with async/await, so I wonder what is better than a try-catch.

Dima method of removing try-catch

Of course, Dima talks about callback hell, Promise chains and finally async/await. When dealing with errors, he didn’t like try-catch, so he wrote a to(Promise) to encapsulate the promise, and then deconstructed the syntax to implement synchronous code similar to the Node error standard. The excerpt code is as follows

// to.js
export default function to(promise) {
    return promise
        .then(data= > {
            return [null, data];
        })
        .catch(err= > [err]);
}Copy the code

Example:

import to from "./to.js";

async function asyncTask(cb) {
    let err, user, savedTask;

    [err, user] = await to(UserModel.findById(1));
    if(! user)return cb("No user found");

    [err, savedTask] = await to(TaskModel({ userId: user.id, name: "Demo Task" }));
    if (err) return cb("Error occurred while saving task");

    if (user.notificationsEnabled) {
        const [err] = await to(NotificationService.sendNotification(user.id, "Task Created"));
        if (err) return cb("Error while sending notification");
    }

    cb(null, savedTask);
}Copy the code

Dima’s approach has a familiar ring to it. Isn’t it common in Node callbacks?

(err, data) => {
    if (err) {
        // deal with error
    } else {
        // deal with data}}Copy the code

So this is really interesting. But if you think about it, every time an error is encountered in this code, the error message is pushed out through the CB () call and the subsequent process is interrupted. Interrupt error handling like this is actually a good fit for try-catch.

Rewrite the above code with a try-catch

To override the above code with a try-catch, first remove the to() wrapper. This way, if an error occurs, you need either to catch promise.prototype.catch () or to catch await Promise statements with try-catch. What is captured, of course, is the REJECT err in every business code.

Note, however, that err is not used directly in the above code, but rather a custom error message. Therefore, err generated by reject is processed into a specified error message. Of course, it’s not difficult for anyone, like

someAsync().catch(err= > Project.reject("specified message"));Copy the code

And then try catch on the top layer. So the rewritten code looks like this:

async function asyncTask(cb) {
    try {
        const user = await UserModel.findById(1)
            .catch(err= > Promise.reject("No user found"));

        const savedTask = await TaskModel({ userId: user.id, name: "Demo Task" })
            .catch(err= > Promise.reject("Error occurred while saving task"));

        if (user.notificationsEnabled) {
            await NotificationService.sendNotification(user.id, "Task Created")
                .catch(err= > Promise.reject("Error while sending notification"));
        }

        cb(null, savedTask);
    } catch(err) { cb(err); }}Copy the code

The above code, in terms of code volume, is not much less than Dima’s code, except that it removes a lot of the if (err) {} structure. Programmers who are not used to try-catch cannot find a breakpoint, but programmers who are used to try-catch know that whenever something goes wrong in a business process (reject in asynchronous code), the code jumps to the catch block to process the reject value.

However, the general business code rejects information that is generally useful. If each of the above business reject’s err is itself an error message, then, using Dima’s schema, you still need to write

if (err) return cb(err);Copy the code

With try-catch, it’s a lot easier

async function asyncTask(cb) {
    try {
        const user = await UserModel.findById(1);
        const savedTask = await TaskModel({ userId: user.id, name: "Demo Task" });

        if (user.notificationsEnabled) {
            await NotificationService.sendNotification(user.id, "Task Created");
        }

        cb(null, savedTask);
    } catch(err) { cb(err); }}Copy the code

Why is that? Because in Dima’s pattern, if (ERR) actually handles two businesses: catching errs that cause interrupts and turning them into error messages, and interrupting business processes by returning. So when the process of converting err to error message is no longer needed, the handler of catching the interrupt and then causing the interrupt again is unnecessary.

Continue to improve

Improve try-catch logic with function expressions

Of course, there is room for improvement, such as the long code in the try {} block, which makes it difficult to read and the try-catch logic feels “cut off”. In this case, functional expressions can be used to improve

async function asyncTask(cb) {
    async function process() {
        const user = await UserModel.findById(1);
        const savedTask = await TaskModel({ userId: user.id, name: "Demo Task" });

        if (user.notificationsEnabled) {
            await NotificationService.sendNotification(user.id, "Task Created");
        }
        return savedTask;
    }

    try {
        cb(null.await process());
    } catch(err) { cb(err); }}Copy the code

If the error handling code is long, it can also be written as a separate function expression.

What if the error handling logic is different at each step of the process

What if an error occurs and instead of being converted to an error message, it is specific error handling logic?

Consider that we use strings to represent error messages, which can be handled later by console.log(). Logic, of course, is best represented by functional expressions, which can ultimately be handled uniformly through calls

async function asyncTask(cb) {
    async function process() {
        const user = await UserModel.findById(1)
            .catch(err= > Promise.reject((a)= > {
                // deal with error on looking for the user
                return "No user found";
            }));

        const savedTask = await TaskModel({ userId: user.id, name: "Demo Task" })
            .catch(err= > Promise.reject((a)= > {
                // making model error
                // deal with it
                return err === 1
                    ? "Error occurred while saving task"
                    : "Error occurred while making model";
            }));

        if (user.notificationsEnabled) {
            await NotificationService.sendNotification(user.id, "Task Created")
                .catch(err= > Promise.reject((a)= > {
                    // just print a message
                    logger.log(err);
                    return "Error while sending notification";
                }));
        }

        return savedTask;
    }

    try {
        cb(null.await process());
    } catch(func) { cb(func()); }}Copy the code

Even more complex cases can be handled

By now we all know. Catch (err => promise.reject (xx)), where xx is the object captured by the try-catch block, so if different services reject different objects, For example, some are functions (representing error handling logic), some are strings (representing error messages), and some are numbers (representing error codes) — all you need to do is change the catch block

    try {
        // ...   
    } catch(something) {
        switch (typeof something) {
            case "string":
                // show message something
                break;
            case "function":
                something();
                break;
            case "number":
                // look up something as code
                // and show correlative message
                break;
            default:
                // deal with unknown error}}Copy the code

summary

I’m not criticizing Dima for its error handling, but it’s a good one, very Node error-handling style, and one that many people will love. Dima’s error handling inspired me to revisit my favorite try-catch approach.

Depending on a variety of factors such as the applicable scenario, team commitments, and personal preferences, different approaches are needed in different situations, and one is not necessarily better than the other — what is right is best!