Skip to content

Extending from Error doesn't work when emitting ES5 #10166

Closed
@mcclure

Description

@mcclure

I am using Typescript 1.8.10. I am using Node v4.4.7 as my primary means of executing Javascript.

Consider this very simple code (I will refer to this later as "code snippet 1"):

class GenConstructFail extends Error {} let x = new GenConstructFail("Test one two"); console.log(x.message)

I run this with tsc test.ts && node test.js. My expected behavior is that this will print "Test one two". My observed behavior is that it prints only a blank line.

There have been issues filed about this before, the ones I found seem to all link back to #5069 where project member @mhegazy closed the issue and explained "The issue is with how these built in types are hand[led] by the engine. They are extensible as per the es6 spec, but do not think the engines are there yet." However, I believe this is not the correct way to think about the issue.

If I look at the ES2015 spec, I find:

19.5.1 The Error Constructor

The Error constructor is the %Error% intrinsic object and the initial value of the Error property of the global object. When Error is called as a function rather than as a constructor, it creates and initializes a new Error object. Thus the function call Error(…) is equivalent to the object creation expression new Error(…) with the same arguments.

The Error constructor is designed to be subclassable. It may be used as the value of an extends clause of a class definition. Subclass constructors that intend to inherit the specified Error behaviour must include a super call to the Error constructor to create and initialize subclass instances with a [[ErrorData]] internal slot.

The last paragraph here is important. The ES2015 spec requires that Error be subclassable using extends and super(). However, it does not specify Error should be subclassable by other means. Here is what tsc emits when run with the default target of ES5 (I will refer to this later as "code snippet 2"):

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var GenConstructFail = (function (_super) {
    __extends(GenConstructFail, _super);
    function GenConstructFail(m) {
        _super.call(this, m);
    }
    return GenConstructFail;
}(Error));
var x = new GenConstructFail("Test one two");
console.log(x.message);

I do not fully understand what the Typescript __extends function is doing, but the generated code for GenConstructCall appears to revolve around expecting it can invoke call on the _super object, in this case Error. However, call is a method on Function.prototype and Error does not have Function.prototype in its prototype chain. Error is an intrinsic object, its prototype is also an intrinsic object (defined in section 19.5.2.1 of the spec), and the operations supported on Error, its constructor, and its prototype are all explicitly enumerated. call is not among them.

So in other words, if the code Typescript generates in ES5 mode does not behave correctly with regard to constructor parameters, it is not because the engines have not yet caught up to ES6, it is because Typescript has generated code which is neither correct ES5 nor correct ES6.

In fact, in my testing, I find that ES6 engines that fully support "code snippet 1" above when simply evaluated as code, do not support "code snippet 2" (the Typescript-transpiled-to-ES5 version of the same code). I tested with Chrome version "51.0.2704.106 m" (up to date) and also with "Microsoft Edge 25.10586.0.0 / Microsoft EdgeHTML 13.10586"; in both cases, code snippet 1 printed "Test one two" and code snippet 2 printed a blank line. (A person I talked to on Twitter tested with Node 5.4.1 and saw these same results.) I also found that in both Chrome and Edge, Error("test") produced an Error object with the message "test" (this is a special behavior guaranteed by section 19.5.1.1 of the ES2015 spec) but Error.call("test") produced an Error object with a blank message.

So, to summarize: Typescript's ES5 target generates an invalid constructor when Error is extended, it is doing this predictably and 100% of the time, and it is invalid because of the Javascript spec and not because of any particular engine limitation. This should be considered a bug.

Moreover, it seems Typescript could be easily modified to fix this. The problem is that .call invocation. Consider this alternate version of code snippet 1 (let's call this "code snippet 3"):

class GenConstructFail extends Error { constructor(m:string) { super(); this.message = m } }
let x = new GenConstructFail("TEST THREE FOUR"); console.log(x.message)

Here is the Javascript tsc emits for code snippet 3 (let's call this "code snippet 4"):

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var GenConstructFail = (function (_super) {
    __extends(GenConstructFail, _super);
    function GenConstructFail(m) {
        _super.call(this);
        this.message = m;
    }
    return GenConstructFail;
}(Error));
var x = new GenConstructFail("TEST THREE FOUR");
console.log(x.message);

The problem with "code snippet 2", at least with V8 and Edge, is that call() is swallowing the arguments to the constructor (which is after all not a real constructor). Assigning the fields after the call() avoids this problem and the code prints "TEST THREE FOUR" even on my old copy of Node 4.4.7.

My suggested fix is that Typescript should detect when it is subclassing Error or one of the built-in Error subclasses (all intrinsics) while in ES5 mode, and in this case generate a constructor which instead of assuming call() works simply takes the arguments to super() and assigns them to this after call(). (And maybe it would be better to find a way to avoid call() altogether if you can, since I see nothing in the spec to guarantee it even exists on Error.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixedFixedA PR has been merged for this issue

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions