Skip to content

Extending Promise gives runtime error: undefined is not a promise #15202

Description

I also asks this on Stackoverflow, but I think this is a bug:

I'm trying to cancel my async method call in Typescript.

To do this, I have created a new Promise type, which inherits from Promise.

My first version was without the Object.setPrototypeOf(..), but I took it for here

TypeScript Version: 2.2.0, targeting ES5

Code

class CancelablePromise<T> extends Promise<T>{

    public cancelMethod: () => void;
    constructor(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) {
        super(executor);
        // Set the prototype explicitly.
        // See: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
        Object.setPrototypeOf(this, CancelablePromise.prototype);
        
    }

    //cancel the operation
    public cancel() {
        if (this.cancelMethod) {
            this.cancelMethod();
        }
    }
}

class Test{

    async postFileAjax<T>(file: File): CancelablePromise <T> { 

        var promiseFunc = (resolve) => { resolve() };
        var promise = new CancelablePromise<T>(promiseFunc);
        promise.cancelMethod = () => { console.log("cancel!") };
    
        return promise;
    }
}

var test = new Test();
test.postFileAjax(null);

Expected behavior:
No runtime error, or compile error if not allowed.

And of course working promise if no error.

Actual behavior:

Got a run-time error

Error:

(unknown) Uncaught TypeError: undefined is not a promise
    at CancelablePromise.Promise (<anonymous>)
    at new CancelablePromise (<anonymous>:44:28)
    at __awaiter (<anonymous>:7:12)
    at Test.postFileAjax (<anonymous>:62:16)
    at <anonymous>:75:6
    at HTMLButtonElement.excuteButton.onclick (https://www.typescriptlang.org/play/playground.js:242)

Link to playground

Activity

  1. mhegazy commented on Apr 26, 2017

    @mhegazy
    Contributor

    The issue here is that ES6 classes (e.g. Promise) are not callable, the way ES5 built in classes (e.g. Array). The emitted code that the compiler produces uses super.call which throws in ES6 engine.

    The fix here is to use Reflect.construct if it exists instead of call. Reflect.construct is not something we can polifill, but we can assume if built-in Promise is there, Refelect.construct is there too.

    Here is a proposal for that from Ron Buckton (@rbuckton):

    var __construct = (this && this.__construct) || (typeof Reflect !== "undefined" && Reflect.construct
        ? function (self, target, args) { return self !== null && Reflect.construct(target, args, self.constructor) || self; }
        : function (self, target, args) { return self !== null && target.apply(self, args) || self; });
    
    var PatchedPromise = (function (_super) {
        __extends(PatchedPromise, _super);
        function PatchedPromise(executor) {
            var _this = this;
            _this = __construct(this, _super, [executor]);
            return _this;
        }
        return PatchedPromise;
    }(Promise));
  2. rbuckton commented on Apr 26, 2017

    @rbuckton
    Contributor

    I have filed #15397 for this proposal.

  3. matthewmueller commented on Jul 30, 2017

    @matthewmueller

    FWIW, another workaround that seems to work is wrapping the promise in a class that's a "thenable". Here's one for a Deferred promise:

    export default class Deferred<T> {
      private res: (value?: T | PromiseLike<T>) => void
      private rej: (reason?: any) => void
      private readonly promise: Promise<T>
    
      constructor() {
        this.promise = new Promise((resolve, reject) => {
          this.res = resolve
          this.rej = reject
        })
      }
    
      then(
        onfulfilled?: (value: T) => T | PromiseLike<T>,
        onrejected?: (reason: any) => PromiseLike<never>
      ): Promise<T> {
        return this.promise.then(onfulfilled, onrejected)
      }
    
      catch(onRejected?: (reason: any) => PromiseLike<never>): Promise<T> {
        return this.promise.catch(onRejected)
      }
    
      resolve(value?: T | PromiseLike<T>): void {
        return this.res(value)
      }
    
      reject(reason?: any): void {
        return this.rej(reason)
      }
    }

    Seems to work with async and whatnot.

  4. mhegazy commented on Nov 20, 2017

    @mhegazy
    Contributor

    closing in favor of #15397

  5. locked and limited conversation to collaborators on Jun 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptES6Relates to the ES6 Spec

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions