es-shims / error-cause

An ES-spec-compliant shim/polyfill/replacement for the `.cause` property on all Error types that works as far down as ES3

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Overwrites constructor

conartist6 opened this issue · comments

Currently the error implementation overwrites constructor, destroying any value that may already be in it.

O.constructor = Error;

This creates problems down the road, since errors usually need to know their constructor as they use it to eliminate stack frames including and above their constructor in the stack trace. The current implementation makes that impossible.

My code boils down to this:

import Error from 'error-cause/Error';

export default class Errawr extends Error {
  constructor(reason, options) {
    super(reason, options);
    
    Error.captureStackTrace(this, this.constructor);
  }
}

In essence Error just isn't considering that it might be a superclass. I think the correct code would be:

O.constructor = this instanceof Error ? this.constructor : Error;

Hmm or perhaps the check should simply be this instanceof Object. I think being more permissive is probably better in this case.

@ljharb I'm going to wait for your feedback on this before I write it up.

Oh right this instanceof Object is a dumb check. It will pass for everything except calling the function without the new keyword.

Maybe I'm not thinking of the problem in general enough terms. The issue isn't anything to do with constructor specifically, it's just that returning a different instance from the constructor destroys the inheritance chain. It'll even screw up instanceof checks down the line.

I see that it is legal to call Error without the new keyword. In such a case you definitely do need to return a newly created object. But the rest of the time I think the constructor should not return anything and instead InstallErrorCause onto this. So in that case this instanceof Object really is the right check to differentiate between those two cases.

instanceof doesn't work across realms; and yes, Error can be called without new.

It would be reasonable to check if O.constructor is already the original Error - and only replace it in that case, I suppose?

It won't be global.Error though, it will be a subtype constructor.

right, i'm saying if it's a subclass, then this.constructor !== OriginalError, and so we wouldn't this.constructor =.

But I still think the problem is bigger than that -- that you can't return a different object at all if subclasses are to be supported properly. Edit: maybe that's not right...

Yes, that's true - I suspect it's not possible to properly support subclasses here though, since i can't use native class syntax.

Let me play with it a bit. I'm pretty sure it is possible. I'll build some test cases.

Here is what I'm imagining, but perhaps you will see problems with this that are as yet invisible to me.

var GetIntrinsic = require('get-intrinsic');
var setProto = require('es-abstract/helpers/setProto');

var $Error = GetIntrinsic('%Error%');

var InstallErrorCause = require('../InstallErrorCause');

function Error(message) {
	var options = arguments.length > 1 ? arguments[1] : void 0;
	if (this == null) {
		return new Error(message, options);
	} else {
		$Error.apply(this, arguments);
		InstallErrorCause(this, options);
	}
}

if (setProto) {
	setProto(Error, $Error);
}

Error.prototype = $Error.prototype;

module.exports = Error;

Ah right, precisely because it can be called without new the usual apply trick won't work.

So it seems that essentially the only thing that works moderately well is to create an error object O and then try to copy all its properties onto this, preferably by enumerating property descriptors and copying them over one by one. But this has poor support, and where it is not supported the resulting "error" will be completely useless (it will contain no stack and no message).

While this shim can't do that, I don't see any problem with using that approach in the library I am writing.

I'm closing this issue as what I've learned is that the problem in my code has nothing to do with this shim. While it works file to write class extends Error, attempts to transpile that code down to ES5 are futile (though they may appear to work).

I agree your library can do something like that.

The main reason I'm not using this in this polyfill is that Errors do, in fact, have internal slots - at the moment, only node exposes that with util.types.isError, but the stacks proposal will add new ways.

Also correct - extending builtins is impossible without native/untranspiled class syntax.

I mean all you have to do to support extension is to create a facade class around the native type. It's not perfect and you lose instanceof, but you gain a lot in return. I've published facades around the common data structures like Map, but so far I've not had cause to use them.

Unless it's a true instance of the native one, then (for anything that's not Error), slot-checking prototype methods will throw.

For example, Map.prototype.get.call(o, k) should work for any o that claims to be a Map or a Map subclass.

It's always something.