How to gracefully add exception handling to all methods of an object

The code will not run as expected, and there may be unexpected situations. In order to ensure the robustness of the program, exception handling is required.

For example, all methods of an object should be subject to exception handling, but it is too troublesome to add try catch to each method:

const obj = {
    aaa() {
        try {
            // aaa
        } catch(e) {
            // xxxx
        } 
    },
    bbb() {
        try {
            // bbb
        } catch(e) {
            // xxxx
        } 
    },
    ccc() {
        try {
            // ccc
        } catch(e) {
            // xxxx
        } 
    }
}

Is there a way to do exception handling for all methods without having to write so many times?

Yes, agent mode.

Proxy mode provides a method with the same name as the target object by wrapping the target object. The final function realization is to call the method of the target object, but some additional responsibilities can be added, such as logs, permissions, etc., to transparently expand the target object.

For example, the high-level components in React are the implementation of agent mode, which can transparently expand the functions of packaged components.

Obviously, the exception handling here can also be done by Proxy. But you don't need to implement it yourself. ES6 provides Proxy, which can be implemented based on it.

Define the createProxy method to implement the Proxy, create a Proxy object, wrap the target object in a layer, and define the processing during get and set:

function createProxy(target) {
    const proxy = createExceptionProxy();
    return new Proxy(target, {
        get: proxy,
        set: proxy
    });
}

function createExceptionProxy() {
    return (target, prop) => {
        if (!(prop in target)) {
            return;
        }

        if (typeof target[prop] === 'function') {
            return createExceptionZone(target, prop);
        }

        return target[prop];
    }
}

If target does not contain prop, null is returned; otherwise, the corresponding attribute value target[prop] is returned.

If the attribute value is a function, make a layer of packaging:

function createExceptionZone(target, prop) {
    return (...args) => {
        let result;
        ExceptionsZone.run(() => {
          result = target[prop](...args);
        });
        return result;
    };
}

The final function is to call target, pass in parameters, and return the call result as the result of the proxy method.

The purpose of wrapping this layer is to handle exceptions, that is, exceptions zone What run does:

class ExceptionsZone {
    static exceptionHandler = new ExceptionHandler();

    static run(callback) {
      try {
        callback();
      } catch (e) {
        this.exceptionHandler.handle(e);
      }
    }
}

Call the target method and do try catch. When an exception occurs, use ExceptionHandler to handle it.

For exception handling here, we will simply print the log:

class ExceptionHandler {
    handle(exception) {
        console.log('Record error:',exception.message, exception.stack);
    }
}

This realizes the purpose of adding exception handling to all methods of the target object.

Under test:

const obj = {
    name: 'guang',
    say() {
        console.log('Hi, I\'m ' + this.name);
    },
    coding() {
        //xxx
        throw new Error('bug');
    }
    coding2() {
        //xxx
        throw new Error('bug2');
    }
}

const proxy = createProxy(obj);

proxy.say();
proxy.coding();

The coding and coding2 methods here will throw exceptions, but they do not handle exceptions. We add the following to it with a proxy:

We successfully added exception handling to object methods through proxy mode!

But now there are still problems. For example, I can't change the coding method to async:

Then what shall I do? Can we uniformly proxy asynchronous and synchronous methods?

There is really no way, because it is impossible to distinguish whether the method is synchronous or asynchronous, and the calling methods of the two methods are also different, but we can provide a runner method to run these asynchronous logic separately:

class ExceptionsZone {
    static exceptionHandler = new ExceptionHandler();

    static async asyncRun(callback) {
      try {
        await callback();
      } catch (e) {
        this.exceptionHandler.handle(e);
      }
    }
}

Then run as follows:

(async function() {
    await ExceptionsZone.asyncRun(proxy.coding2);
})();

In this way, exceptions in asynchronous logic can be handled:

We add exception handling to all synchronous methods of the object through proxy, and then provide the runner function running asynchronous methods to handle asynchronous exceptions. Combined with these two methods, we gracefully add exception handling to all methods of the target object.

You may say that the agent is the agent. Why do you define so many class es?

Because this logic is from nest JS source code. This is how it adds exception handling to objects in the source code:

Asynchronous logic also provides a separate method to run:

I think the transparent method of adding exception handling to objects is very elegant, so I changed it from nest JS source code.

summary

In order to ensure robustness, we need to add exception handling to all possible error reporting codes, but it is too troublesome to add try catch to each method, so we use Proxy to implement Proxy and add exception handling to all methods of the object transparently.

However, the agent only adds synchronous exception handling and does not capture asynchronous logic exceptions. We can run asynchronous methods individually.

Combining the two methods of agent + runner running asynchronous methods can add exception handling to an object that does not do any exception handling. Isn't it elegant

Added by theoph on Thu, 03 Mar 2022 11:59:13 +0200