kjo
kjo

Reputation: 35331

Can an object *truly* inherit from Error.prototype?

[There are other similar questions on this topic, but none of them answer the question I'm asking here, AFAICT. (I.e. the answers I have read all explain why a particular construct fails to do with the questioner is trying to do, and in some cases they offer alternative ways to get the desired results. But no thread answers the question of how to achieve true inheritance from Error.prototype.)]

The best I've managed to do is still basically useless:

function MyErr(message) {
  var tmp = Error.apply(this, arguments);
  for (var prop in tmp) { this[prop] = tmp[prop]; }
}
MyErr.prototype = Object.create(Error.prototype);

Even after "manually" copying properties (a sure sign of a "busted inheritance"), the object returned by new MyErr("whatever") is still not even remotely close to what I would consider "an instance that inherits from Error.prototype". The deviations from expected behavior are really too many to list here—they pop up everywhere I look!—, but, for starters,

console.log((new Error("some error")).toString())     // "Error: some error"
console.log((new MyErr("another error")).toString())  // "Error"
                                                      // expected
                                                      // "MyError: another error"

console.log((new Error("some error")).constructor)    // "Error()"
console.log((new MyErr("another error")).constructor) // "Error()"
                                                      // expected:
                                                      // "MyError()"

(In case anyone is wondering, no, constructor is not one of the properties that gets copied in MyErr's for loop. I checked.)

Can an object truly inherit from Error.prototype?

Given that to answer the analogous question for Array requires a lengthy treatise, I can't reasonably expect a full answer to my question here, but I hope I can get a pointer to such full answer.


UPDATE: I tried out Bergi's proposal. Below I provide a full, self-contained implementation.1

<!DOCTYPE html>
<html><head><meta charset="utf-8"></head><body>
  <script src="http://code.jquery.com/jquery-latest.min.js"></script>
  <script>
    jQuery(document).ready(function ($) {
      function MyErr(message) {
        var tmp = Error.apply(this, arguments);
        Object.getOwnPropertyNames(tmp).forEach(function(p) {
          console.log('property: ' + p);
          Object.defineProperty(this, p,
                                Object.getOwnPropertyDescriptor(tmp, p));
        }, this);
        console.log('done creating ' + this);
      }
      MyErr.prototype = Object.create(Error.prototype, {
        constructor: {value:MyErr, configurable:true},
        name: {value:"MyErr", configurable:true}
      });

      var error = new Error("some error");
      var myerr = new MyErr("another error");

      console.log(error.toString());
      console.log(myerr.toString());

      console.log(error.constructor);
      console.log(myerr.constructor);
    });
  </script></body></html>

The output I get in the Firebug console is this:

done creating MyErr                                         testjs.html (line 13)
Error: some error                                           testjs.html (line 23)
MyErr                                                       testjs.html (line 24)
Error()                                                     testjs.html (line 26)
MyErr(message)                                              testjs.html (line 27)

Note in particular that the output does not include any lines beginning with property:. This means that the console.log statement within the forEach loop in MyErr never gets executed (and presumably the same goes for the rest of the forEach callback).

If I replace the forEach loop with

        for (var p in tmp) {
          console.log('property: ' + p);
          Object.defineProperty(this, p,
                                Object.getOwnPropertyDescriptor(tmp, p));
        };

...then three new lines get prepended to the output (line numbers omitted):

property: fileName
property: lineNumber
property: columnNumber

This shows that not even message is among the tmp properties accessible this way (though message does show up among tmp's properties in Firebug's variable inspector, along with a bazillion of other ones that are also not shown in the console output above).

Also, the explicitly-set name property does make an appearance in the output of myerr.toString(), but still this method does not behave in the same way as error.toString().

I do get the expected output from myerr.toString() if I add the following line at the end of the MyErr function:

this.message = message;

...but I get little comfort from this, because this maneuver, along with most of the other maneuvers I've shown above, have been ad hoc kluges aimed at the specific examples that I posted, but these were intended only as an illustration of what appears to be a deeper problem, namely a complete breakdown of the expected inheritance model.

The purpose of this question is to find out how to achieve "true inheritance" from Error.prototype, and if that is not possible, then to simulate it as closely as possible, having a clear picture of the extent to which the simulation fails to implement the full inheritance model. I stress that the latter of these two alternatives should not be confused with the strategy of incrementally patching some flawed implementation every time we happen to notice some new way in which it fails to conform to expectation. This "incremental patching" approach provides the ideal environment for silent bugs, and is thus a recipe for insanity.


UPDATE2: There seems to be no end to JS's unpredictability. I just discovered that, at least in the Firebug console, Object.getOwnPropertyNames produces a different value depending on whether its argument is a non-atomic expression or a variable to which the same non-atomic expression has been previously assigned.     wtf?

For example, the following is copy-pasted directly from an interaction in the Firebug console (I've added one empty line before each command, for ease of reading):

>>> Object.getOwnPropertyNames(new Error("not assigned"))
[]

>>> errorvar = new Error("assigned")
Error: assigned
(no source for debugger eval code)

>>> Object.getOwnPropertyNames(errorvar)
["fileName", "lineNumber", "message", "stack"]

(The output right after the assignment to errorvar seems to have to do with the fact that an Error object was created, even though it was not thrown. The same output appears even if one deletes everything to the left of new in that line.)

And if that were not enough, if I run this from within a script

console.log(Object.getOwnPropertyNames(new Error("not assigned")))
var errorvar = new Error("assigned");
console.log(Object.getOwnPropertyNames(errorvar));

the output in the Firebug console is

[ ]
[ ]

I don't know if this erratic behavior is due to ECMAScript5, or JavaScript, or Firefox, or Firebug, or what, but it's driving me insane...


1 I posted this self-contained implementation to encourage others to try it out. If there is one thing I have learned about JavaScript is that, no matter how simple the code, and no matter how much you think you know about JavaScript, the only way to know what some JavaScript code is going to do is to run it. Sadly, JavaScript programming still remains an embarrassingly experimental activity. JS is not for the armchair programmer! :)

Upvotes: 2

Views: 688

Answers (5)

T.J. Crowder
T.J. Crowder

Reputation: 1075209

As of ES2015 (aka "ES6"), yes, you can truly subclass Error (and Array). On any recent Chrome, Firefox, Safari, or Edge:

class MyError extends Error {
  myOwnMethod() {
    console.log("MyError method");
  }
}
try {
  throw new MyError("Thrown");
}
catch (e) {
  console.log(e instanceof Error);   // true
  console.log(e instanceof MyError); // true
  console.log(e.message);            // "Thrown"
  e.myOwnMethod();
}


Re this part of your "Update":

This shows that not even message is among the tmp properties accessible this way (though message does show up among tmp's properties in Firebug's variable inspector, along with a bazillion of other ones that are also not shown in the console output above).

That tells us that message wasn't an "own" property (so didn't show up in the result of Object.getOwnPropertyNames), and wasn't enumerable (so didn't show up in for-in). So apparently it was a non-enumerable inherited property.

If so, that was a spec violation that's since been fixed. The spec at the time was quite clear: Calling Error without new is just like calling it with new, and when you call Error with new and pass it an argument, it's supposed to set an own property called message on the newly-created instance, initialized with String(theArgumentYouPassed).

Here in 2017, it's correct: Error("foo").hasOwnProperty("message") is true on Chrome and Firefox, which is in keeping with both the old spec and the current one.


Re your "Update 2":

For example, the following is copy-pasted directly from an interaction in the Firebug console (I've added one empty line before each command, for ease of reading):

>>> Object.getOwnPropertyNames(new Error("not assigned"))
[]

>>> errorvar = new Error("assigned")
Error: assigned
(no source for debugger eval code)

>>> Object.getOwnPropertyNames(errorvar)
["fileName", "lineNumber", "message", "stack"]

If it did that in 2013 in Firebug, you'll be glad to know it doesn't do that in 2017 in Firefox's built-in developer tools. Nor in Chrome's. The results of the two calls to Object.getOwnPropertyNames is consistent (now). The behavior you described seeing is bizarre, I wish I'd had a chance to try it with 2013's Firefox+Firebug. It's extremely hard to buy that simply assigning the object reference to a variable would have an effect on what you get back when passing that reference into Object.getOwnPropertyNames.

Upvotes: 1

Esailija
Esailija

Reputation: 140236

Well I don't know what else you could need apart from the stack trace and instanceof. Try this:

function MyError(message) {
    this.name = this.constructor.name;
    this.message = message;
    if (Error.captureStackTrace) {
        Error.captureStackTrace(this, this.constructor);
    } else {
        var stack = new Error().stack;
        if (typeof stack === "string") {
            stack = stack.split("\n");
            stack.shift();
            this.stack = stack.join("\n");
        }
    }
}
MyError.prototype = Object.create(Error.prototype, {
    constructor: {
        value: MyError,
        writable: true,
        configurable: true
    }
});
try {
    throw new MyError("message");
} catch (e) {
    console.log(e + "");
    console.log(e.stack + "");
}
try {
    throw new Error("message");
} catch (e) {
    console.log(e + "");
    console.log(e.stack + "");
}

http://jsfiddle.net/VjAC7/1/

Chrome

MyError: message
MyError: message
    at window.onload (http://fiddle.jshell.net/VjAC7/1/show/:44:11)
Error: message
Error: message
    at window.onload (http://fiddle.jshell.net/VjAC7/1/show/:51:11)

Firefox

MyError: message
window.onload@http://fiddle.jshell.net/VjAC7/1/show/:44
Error: message
window.onload@http://fiddle.jshell.net/VjAC7/1/show/:51

IE10

Error: message 
Error: message
    at onload (http://fiddle.jshell.net/VjAC7/1/show/:44:5) 
Error: message 
Error: message
    at onload (http://fiddle.jshell.net/VjAC7/1/show/:51:5) 

Upvotes: 4

MichaC
MichaC

Reputation: 13410

Instead of trying to inherit from error, try to extend a new error object and return it I wrote a quick example how to achieve this here: http://jsfiddle.net/Elak/tvXqC/2/

Basically you can extend your returned object and also overwrite the error name (which then gets used in console or wherever you log that error

var Errors = {};
(function(){
    var myError = function(message){
        return $.extend(new Error(arguments[0]),{
            printExample : function(){
                $("#out").html("Error: " + this.name + ": " + this.message);
            }
        },
        // overwriting error object properties. default for name is Error...
        // this way you can control the log output
        {
            name: "ExceptionNameWhichWillShowInLog"
        }
        );
    };
    Errors.MyError = myError;
})();

the object with the function printExample is now the place for your own methods (replaces the prototype implementation...).

You have actually the same access to this where this is now the instance of a "real" error object and you can do with that instance whatever you want...

Upvotes: 0

Bergi
Bergi

Reputation: 665130

Use this:

function MyErr(message) {
  var tmp = Error.apply(this, arguments);
  Object.getOwnPropertyNames(tmp).forEach(function(p) {
    Object.defineProperty(this, p, Object.getOwnPropertyDescriptor(tmp, p));
  }, this);
}
MyErr.prototype = Object.create(Error.prototype, {
  constructor: {value:MyErr, configurable:true},
  name: {value:"MyErr", configurable:true}
});

This will copy even non-enumerable properties from tmp to this, it will reset the .constructor property and it will set the .name property as expected by .toString.

> (new MyErr("another error")).toString()
"MyErr: another error"
> (new MyErr("another error")).constructor
Function MyErr

Upvotes: 0

Jivings
Jivings

Reputation: 23260

To me it seems as if the Object created below inherits all the properties of the Error object and behaves identically... Perhaps you can explain how/if it differs?

var MyErr = function(message) {
  this.name = 'My Error';
  this.message = message;
}
MyErr.prototype = new Error();
MyErr.prototype.constructor = MyErr;

throw new MyErr('Uhoh!') // My Error: Uhoh!

On the prototype I can see get stack and set stack inhertited from Error.

Upvotes: 2

Related Questions