16

I'm learning about classes and inheritance in javascript. I thought that the following is a fairly standard way of extending an existing object as I got the style from the MDN docs on Object.create

I was expecting to see 'ok' and then 'Yay! Hello' in the console, but instead I go this error:

Uncaught TypeError: #<MyPromise> is not a promise
at new MyPromise (<anonymous>:5:17)
at <anonymous>:19:6

It looks like the Promise constructor is throwing an exception because it can tell that the object I've given it to initialise isn't a straightforward Promise.

I want the Promise constructor to initialise my object as if it was a Promise object, so I can then extend the class. Why wouldn't they write the Promise constructor to work with this common pattern? Am I doing something wrong? Cheers for taking a look!

MyPromise = function(message, ok) {
    var myPromise = this;
    this.message = message;
    this.ok = ok;
    Promise.call(this, function(resolve, reject) {
        if(this.ok) {
            console.log('ok');
            resolve(myPromise.message);
        } else {
            console.log('not ok');
            reject(myPromise.message);
        }   
    }); 
};  

MyPromise.prototype = Object.create(Promise.prototype);
MyPromise.prototype.constructor = MyPromise;

(new MyPromise('Hello', true))
    .then(function(response) {console.log('Yay! ' + response);})
    .except(function(error) {console.log('Aww! ' + error);});

I was originally trying to make a BatchAjax class that you could use like:

(new BatchAjax([query1, query2]))
    .then(function(response) {console.log('Fires when all queries are complete.');}); 

It was just a bit of fun really.

tobuslieven
  • 1,474
  • 2
  • 14
  • 21
  • 1
    What exactly is your use case for extending `Promise`? – Bergi Jan 22 '17 at 15:07
  • I wanted to make a BatchAjax class that you could use like `(new BatchAjax([query1, query2])).then(function(response) {console.log('Fires when all queries are complete.');});` It was just a bit of fun really. – tobuslieven Jan 22 '17 at 17:23
  • 1
    That sounds like it should be a simple static function that returns a normal promise. No subclassing required. – Bergi Jan 22 '17 at 17:53
  • 1
    Isn't that `Promise.all`? (But if just for fun, fair enough, experimentation is educational.) – T.J. Crowder Jan 22 '17 at 17:59
  • @Bergi Yeah that's the first thing I did but I thought it would be more fun to try to extend the class. I wanted to beat this hard won Object.create knowledge into my head. I'd really appreciate you taking a look at the alternative solution in my answer. I want to know the problems with this approach. – tobuslieven Jan 22 '17 at 23:21
  • @T.J.Crowder Hehe yep, this is just for fun, even if it took my whole weekend. I feel like I've learned a lot. But the stuff I had in mind is subtly different from `Promise.all` if I understand correctly. I was thinking that it should only resolve once all async is finished, even if some of them error. Not that this is necessarily useful, just different. It would be great if you could look at my answer, it seems to work, but I really want to know the drawbacks too. Cheers for answering, it pointed me in an interesting direction. – tobuslieven Jan 22 '17 at 23:28
  • 1
    FYI, your specific use case here can be solved just fine without subclassing a promise. The example you've shown is often called `Promise.settle()` and there are many implementations of that without any subclassing. Here are [several implementations in this answer](http://stackoverflow.com/questions/36605253/es6-promise-all-error-handle-is-settle-needed/36605453#36605453). – jfriend00 Jan 23 '17 at 23:07
  • @jfriend Cheers, it's interesting to see other ways of doing this. I got it working without inheritance way before I got the example in the question working. But it's worth explicitly pointing that out. I think I have a solution looking for a problem. I'd really like to know a genuine use case for inheriting from a Promise. edit: Oops thought this was a comment on a different question, this one: [Is this an extendible Promise](http://stackoverflow.com/questions/41816158/is-this-an-inheritable-promise) – tobuslieven Jan 23 '17 at 23:23
  • 1
    @tobuslieven - Yeah, to implement `.settle()` the usual solution is to take your array of promises and add a `.catch()` handler to each of them that makes all of them succeed and then pass the resulting promises to `Promise.all()`. Since they all succeed, it will tell you when they are all done. The variation in implementation depends upon what info you want when it is all said and done about which succeeded, which failed and with what errors. The simpler the info you want back, the simpler an implementation you can get by with. The answer I linked shows you the range of implementations. – jfriend00 Jan 23 '17 at 23:40

3 Answers3

12

The native Promise class (like Error and Array) cannot be correctly subclassed with the old ES5-style mechanism for subclassing.

The correct way to subclass Promise is through class syntax:

class MyPromise extends Promise {
}

Example:

class MyPromise extends Promise {
    myMethod() {
        return this.then(str => str.toUpperCase());
    }
}

// Usage example 1
MyPromise.resolve("it works")
    .myMethod()
    .then(result => console.log(result))
    .catch(error => console.error(error));
    
// Usage example 2
new MyPromise((resolve, reject) => {
    if (Math.random() < 0.5) {
        resolve("it works");
    } else {
        reject(new Error("promise rejected; it does this half the time just to show that part working"));
    }
})
    .myMethod()
    .then(result => console.log(result))
    .catch(error => console.error(error));

If it's your goal to do that without class, using mostly ES5-level features, you can via Reflect.construct. Note that Reflect.construct is an ES2015 feature, like class, but you seem to prefer the ES5 style of creating classes.

Here's how you do that:

// Create a constructor that uses `Promise` as its super and does the `super` call
// via `Reflect.construct`
const MyPromise = function(executor) {
    return Reflect.construct(Promise, [executor], MyPromise);
};
// Make `MyPromise` inherit statics from `Promise`
Object.setPrototypeOf(MyPromise, Promise);
// Create the prototype, add methods to it
MyPromise.prototype = Object.create(Promise.prototype);
MyPromise.prototype.constructor = MyPromise;
MyPromise.prototype.myMethod = function() {
    return this.then(str => str.toUpperCase());
};

Then use it just like Promise:

MyPromise.resolve("it works")
    .myMethod()
    .then(result => console.log(result))
    .catch(error => console.error(error));

or

new MyPromise(resolve => resolve("it works"))
    .myMethod()
    .then(result => console.log(result))
    .catch(error => console.error(error));

etc.

Live Example:

// Create a constructor that uses `Promise` as its super and does the `super` call
// via `Reflect.construct`
const MyPromise = function(executor) {
    return Reflect.construct(Promise, [executor], MyPromise);
};
// Make `MyPromise` inherit statics from `Promise`
Object.setPrototypeOf(MyPromise, Promise);
// Create the prototype, add methods to it
MyPromise.prototype = Object.create(Promise.prototype);
MyPromise.prototype.constructor = MyPromise;
MyPromise.prototype.myMethod = function() {
    return this.then(str => str.toUpperCase());
};

// Usage example 1
MyPromise.resolve("it works")
    .myMethod()
    .then(result => console.log(result))
    .catch(error => console.error(error));
    
// Usage example 2
new MyPromise((resolve, reject) => {
    if (Math.random() < 0.5) {
        resolve("it works");
    } else {
        reject(new Error("promise rejected; it does this half the time just to show that part working"));
    }
})
    .myMethod()
    .then(result => console.log(result))
    .catch(error => console.error(error));

If you want to avoid changing the prototype of MyPromise, you can copy the static properties over, but it's not quite the same thing:

// Create a constructor that uses `Promise` as its super and does the `super` call
// via `Reflect.construct`
const MyPromise = function(executor) {
    return Reflect.construct(Promise, [executor], MyPromise);
};
// Assign the statics (`resolve`, `reject`, etc.) to the new constructor
Object.assign(
    MyPromise,
    Object.fromEntries(
        Reflect.ownKeys(Promise)
            .filter(key => key !== "length" && key !== "name")
            .map(key => [key, Promise[key]])
    )
);
// Create the prototype, add methods to it
MyPromise.prototype = Object.create(Promise.prototype);
MyPromise.prototype.constructor = MyPromise;
MyPromise.prototype.myMethod = function() {
    return this.then(str => str.toUpperCase());
};

Using it is the same, of course.

Live Example:

// Create a constructor that uses `Promise` as its super and does the `super` call
// via `Reflect.construct`
const MyPromise = function(executor) {
    return Reflect.construct(Promise, [executor], MyPromise);
};
// Assign the statics (`resolve`, `reject`, etc.) to the new constructor
Object.assign(
    MyPromise,
    Object.fromEntries(
        Reflect.ownKeys(Promise)
            .filter(key => key !== "length" && key !== "name")
            .map(key => [key, Promise[key]])
    )
);
// Create the prototype, add methods to it
MyPromise.prototype = Object.create(Promise.prototype);
MyPromise.prototype.constructor = MyPromise;
MyPromise.prototype.myMethod = function() {
    return this.then(str => str.toUpperCase());
};

// Usage example 1
MyPromise.resolve("it works")
    .myMethod()
    .then(result => console.log(result))
    .catch(error => console.error(error));
    
// Usage example 2
new MyPromise((resolve, reject) => {
    if (Math.random() < 0.5) {
        resolve("it works");
    } else {
        reject(new Error("promise rejected; it does this half the time just to show that part working"));
    }
})
    .myMethod()
    .then(result => console.log(result))
    .catch(error => console.error(error));
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Cheers for that information. I don't like the idea of being forced to change coding style for an exception where it's not supported anymore. We'll end up with an unpleasant mixture of syntaxes. They should just support the original way instead of adding ingredients and stirring the pot. A simplified system _added_ to a complex system is _more complicated_ than the original complex system on it's own, not less. People always make this mistake. /rant I wonder if I can get round it by composing a Promise inside my class instead of extending it. – tobuslieven Jan 22 '17 at 14:43
  • 3
    @tobuslieven The "original syntax" for extending classes never did support builtin constructors, and is not suited to do that; that's why ES6 uses a new approach for this. – Bergi Jan 22 '17 at 15:06
  • @Bergi My rant resumes: The original syntax isn't very well suited to anything much, but it's the mechanism that the language had and should have been made to work in all cases (in my not very humble opinion). If it didn't work before and they were designing a new version of the language (ES6) then they should have made changes to the builtins to make them work with the old syntax. Not introduce an alternative competing syntax. Javascript's inheritance syntax was a dog, now it's two dogs. – tobuslieven Jan 22 '17 at 17:33
  • 2
    @tobuslieven: What you're proposing was impossible without breaking changes. Moving JavaScript forward without breaking a huge amount of existing code is **tremendously challenging** and a challenge TC39 takes very seriously. Re "two dogs" -- the old syntax was a hack we thankfully no longer have to employ. There's no need for it going forward. – T.J. Crowder Jan 22 '17 at 17:58
  • @T.J.Crowder Fair enough. It's just frustrating that the old syntax doesn't work and the new syntax isn't widely supported enough yet to fully rely on. I'd like to know your take on my alternative answer using composition. Serious fine grained critique is very welcome. Cheers! – tobuslieven Jan 22 '17 at 23:33
  • @T.J.Crowder I may be wrong but I'm wondering if my solution implies I could create my own Promise class that does allow "normal" inheritance. – tobuslieven Jan 22 '17 at 23:50
  • 1
    when I try to call MyPromise.then, browser throws error `Uncaught TypeError: Promise resolve or reject function is not callable at Promise.then ()` – Disorder Mar 11 '20 at 08:18
  • @Disorder - Promises don't have a static `then` method. It's a prototype method. – T.J. Crowder Mar 11 '20 at 08:43
  • @T.J.Crowder yes, I should write `myPromise` instead of `MyPromise`. Just a mistype here, in my comment, but not in code ;) So, if you try to extend a promise, you'll get the same error. – Disorder Mar 14 '20 at 15:35
  • @tobuslieven - FWIW, I added an example of properly (I think) subclassing `Promise` witout using `class`. Not sure why I had trouble with `Reflect.construct` back in 2017. – T.J. Crowder Mar 15 '20 at 10:32
  • @Disorder - I'm afraid I don't know what's going on with your promise subclass. The subclass above doesn't have any problem with `then`. – T.J. Crowder Mar 15 '20 at 10:34
5

My latest solution is to compose a Promise object into my class as this.promise and then pretend to be inheriting from Promise by overriding all the instance methods of Promise and passing them on to the this.promise object. Hilarity ensues. I'd really welcome people pointing out the drawbacks to this approach.

Nothing is too obvious for me to have missed.

When I paste this code into the Chrome console, it seems to work. That's as far as I comprehend.

Cheers for taking a look.

BatchAjax = function(queries) {
    var batchAjax = this;
    this.queries = queries;
    this.responses = [];
    this.errorCount = 0;
    this.promise = new Promise(function(resolve, reject) {
        batchAjax.executor(resolve, reject);
    });
};
BatchAjax.prototype = Object.create(Promise.prototype);
BatchAjax.prototype.constructor = BatchAjax;
BatchAjax.prototype.catch = function(fail) {
    return this.promise.catch(fail);
}
BatchAjax.prototype.then = function(success, fail) {
    return this.promise.then(success, fail);
};
BatchAjax.prototype.executor = function(resolve, reject) {
    var batchAjax = this;
    $.each(this.queries, function(index) {
        var query = this;
        query.success = function (result) {
            batchAjax.processResult(result, index, resolve, reject);
        };
        query.error = function (jqXhr, textStatus, errorThrown) {
            batchAjax.errorCount++;
            var result = {jqXhr: jqXhr, textStatus: textStatus, errorThrown: errorThrown};
            batchAjax.processResult(result, index, resolve, reject);
        };
        $.ajax(query);
    });
};
BatchAjax.prototype.processResult = function(result, index, resolve, reject) {
    this.responses[index] = result;
    if (this.responses.length === this.queries.length) {
        if (this.errorCount === 0) {
            resolve(this.responses);
        } else {
            reject(this.responses);
        }
    }
};

// Usage
var baseUrl = 'https://jsonplaceholder.typicode.com';
(new BatchAjax([{url: baseUrl + '/todos/4'}, {url: baseUrl + '/todos/5'}]))
    .then(function(response) {console.log('Yay! ', response);})
    .catch(function(error) {console.log('Aww! ', error);});
tobuslieven
  • 1,474
  • 2
  • 14
  • 21
  • 2
    This answer seems, to me, to be the correct answer. This strategy is the only one I have found that supports a) calling the super-constructor and accessing the `resolve` and `reject` functions and b) passing `instanceof Promise` checks. My use case is about extending the `Promise` class into a `Deferred` class that exposes the `resolve` and `reject` functions of its instances. – Jacoscaz Apr 18 '19 at 16:28
  • 2
    I should add that, although I think this answer describes the best way to extend the Promise class, doing so is probably not the most advisable thing under 99.9% of circumstances. Still, if one absolutely has to do it, this way works best (at least according to my testing). – Jacoscaz Apr 18 '19 at 16:49
  • I was going to add some edit suggestion, however, the suggested edit queue is full, so I had to add a new answer below. Thanks for your inspiration. – Alvan Jan 11 '21 at 06:49
2

As @tobuslieven's answer suggested, it's not necessary to extend Promise. Below is a simplified sample.

ES6:

class MyPromise {

  async executor () {
    if(!this.name) {
      throw new Error('whoops!')
    }
    console.log('Hello', this.name)
  }

  then () {
    const promise = this.executor()
    return promise.then.apply(promise, arguments)
  }

  catch () {
    const promise = this.executor()
    return promise.catch.apply(promise, arguments)
  }
}

ES5:

function MyPromise () { }

MyPromise.prototype.executor = function () {
  var self = this
  return new Promise(function (resolve, reject) {
    if (!self.name) {
      return reject(new Error('whoops!'))
    }
    console.log('Hello', self.name)
    resolve()
  })
}

MyPromise.prototype.then = function () {
  var promise = this.executor()
  return promise.then.apply(promise, arguments)
}

MyPromise.prototype.catch = function () {
  var promise = this.executor()
  return promise.catch.apply(promise, arguments)
}

both can be tested by:

var promise = new MyPromise()
promise.name = 'stackoverflow'
promise
  .then(function () {
    console.log('done!')
  })
  .catch(console.log)
Alvan
  • 345
  • 3
  • 7