Search code examples
javascriptinheritanceerror-handlinggoogle-closure-compiler

Extending Error when using Closure Compiler


I'm using the Closure Compiler and trying to do subclassing of the Error 'class'. I have a meta-function that tries to do that. It looks like this:

/**
 * @param {string} name
 * @param {(function(new:Error)?)=} parent
 * @param {(Function?)=} constructor
 * @return {function(new:Error)}
 * @usage
 *      var MyError = subclassError('MyError', null, function (x, y) {
 *          this.message = prop1 + " " + prop2;
 *      });
 *      throw new MyError(new Error(), 1, 2);
**/
function subclassError (name, parent, constructor) {
    // allow subclassing of other errors
    if (!parent) parent = Error;
    // allow no constructor to be provided
    if (!constructor) {
        /** @this {Error} */
        constructor = function (msg) { if (msg) this.message = msg; };
    }
    /** 
     * @constructor
     * @extends {Error}
     * @param {Error} error
     * @param {...*} var_args
    **/
    var func = function (error, var_args) {
        // this check is just a guard against further errors
        // note that we don't use something like getOwnPropertyNames
        // as this won't work in older IE
        var propsToCopy = ['fileName', 'lineNumber', 'columnNumber', 
           'stack', 'description', 'number', 'message'];
        for (var i = 0; i < propsToCopy.length; i++) {
            this[propsToCopy[i]] = error[propsToCopy[i]];
        }
        this.name = name;
        constructor.apply(this, Array.prototype.slice.call(arguments, 1));
    };
    func.prototype = Object.create(parent.prototype);
    func.prototype.constructor = func;
    func.name = constructor.name;
    return func;
}

Basically, the above function creates a subclass of Error, which when called requires passing in a native Error object. From this object it fills in stuff like line number, stack trace, etc. But it also allows you to pass in other parameters.

Here's a sample of using it:

/**
 * @constructor
 * @extends {Error}
 * @param {Error} err
 * @param {number} bar
 * @param {number} baz
**/
var FooError = subclassError('FooError', null, function (bar, baz) {
    this.message = "invalid bar: '" + bar + "', (using '" + baz + "')";
});

/**
 * Frobs the noid.
 * @param {number} x
 * @param {number} y
 * @throws FooError
**/
function frob (x, y) {
    if (x < 0) throw new FooError(new Error(), x, y);
}

When I compile this with Closure like so:

java -jar compiler.jar
    --compilation_level ADVANCED_OPTIMIZATIONS --warning_level VERBOSE
    --language_in ECMASCRIPT5 --language_out ECMASCRIPT3
    --js_output_file=frob.min.js frob.js

I get the following warnings:

frob.js:39: WARNING - inconsistent return type
found   : function (new:func, (Error|null), ...*): undefined
required: function (new:Error): ?
    return func;
           ^

frob.js:60: WARNING - Function FooError: called with 3
        argument(s). Function requires at least 0 argument(s) and no more
        than 0 argument(s).
    if (x < 0) throw new FooError(new Error(), x, y);

One of the tricky things here is that although I know (from the code) that the func object descends from Error, since either a descendant of Error was passed in or we use Error as the parent, Closure thinks that it's an instance of func and that this not an instance of Error. I tried adding an @extends {Error} in the above to rectify the problem, but Closure still thinks !(func instanceof Error). This is the first warning.

In the second warning, the problem is that it doesn't recognize that FooError takes three arguments. I tried to add three parameters to FooError, but since Closure doesn't see the parameter list, it can't find that out.

Is there a way to get rid of these warnings by telling Closure more information? Or is there a way to extend Error in Closure in a simpler way that allows us to get the line number, stack, etc.?


Solution

  • Because you are calling a method that returns a constructor, we have to be a little tricky in order to get full type checking in the compiler.

    First, change the subclassError method to return the unknown type so that we can define the type signature independently:

    /**
     * @param {string} name
     * @param {(function(new:Error)?)=} parent
     * @param {(Function?)=} constructor
     * @return {?}
     **/
    function subclassError (name, parent, constructor) {}
    

    Next, we add a stub definition for FooError so that the compiler can understand type information. Then we actually assign the result of the subclassError method.

    /**
     * @constructor
     * @extends {Error}
     * @param {Error} err
     * @param {number} bar
     * @param {number} baz
     **/
    var FooError = function(err, bar, baz) {};
    
    FooError = subclassError('FooError', null, function (bar, baz) {
      this.message = "invalid bar: '" + bar + "', (using '" + baz + "')";
    });
    

    Now we get correct type checking from the compiler

    // The compiler will warn that FooError is called with
    // the wrong number of arguments
    new FooError(new Error());
    

    See a Full Example