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.?
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());