Extending a Promise in javascript

13,309

Solution 1

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));

Solution 2

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);});

Solution 3

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)
Share:
13,309
tobuslieven
Author by

tobuslieven

Hi. I've made some rigging tutorials for Blender 3D on YouTube. I'm interested in games and worked on one called SmashTrix. Notes Stern-Levinson paper on criteria for planets. Use of Hubble time and 1 radian subjective? Need to ask a question about this. Seems to be based on a measure of likelihood that a large object deflects a small object by one radian, over a period of one Hubble time.

Updated on June 25, 2022

Comments

  • tobuslieven
    tobuslieven almost 2 years

    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
    tobuslieven over 7 years
    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.
  • Bergi
    Bergi over 7 years
    @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.
  • tobuslieven
    tobuslieven over 7 years
    @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.
  • T.J. Crowder
    T.J. Crowder over 7 years
    @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.
  • tobuslieven
    tobuslieven over 7 years
    @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
    tobuslieven over 7 years
    @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.
  • Jacoscaz
    Jacoscaz about 5 years
    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
    Jacoscaz about 5 years
    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).
  • Disorder
    Disorder about 4 years
    when I try to call MyPromise.then, browser throws error Uncaught TypeError: Promise resolve or reject function is not callable at Promise.then (<anonymous>)
  • T.J. Crowder
    T.J. Crowder about 4 years
    @Disorder - Promises don't have a static then method. It's a prototype method.
  • Disorder
    Disorder about 4 years
    @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.
  • T.J. Crowder
    T.J. Crowder about 4 years
    @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
    T.J. Crowder about 4 years
    @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.
  • Alvan
    Alvan over 3 years
    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.