2

I'm learning JavaScript, and I decided that an excelent chalenge would be to implement a custom Promise class in JavaScript. I managed to implement the method then, and it works just fine, but I'm having difficulties with the error handling and the method catch. Here is my code for the Promise class (in a module called Promise.mjs):

export default class _Promise {
  constructor(executor) {
    if (executor && executor instanceof Function) {
      try {
        executor(this.resolve.bind(this), this.reject.bind(this));
      } catch (error) {
        this.reject(error);
      }
    }
  }
  resolve() {
    if (this.callback && this.callback instanceof Function) {
      return this.callback(...arguments);
    }
  }
  reject(error) {
    if (this.errorCallback && this.errorCallback instanceof Function) {
      return this.errorCallback(error);
    } else {
      throw `Unhandled Promise Rejection\n\tError: ${error}`;
    }
  }
  then(callback) {
    this.callback = callback;
    return this;
  }
  catch(errorCallback) {
    this.errorCallback = errorCallback;
    return this;
  }
}

When I import and use this class in the following code, all the then() clauses run as according, and I get the desired result in the console:

import _Promise from "./Promise.mjs";

function sum(...args) {
    let total = 0;
    return new _Promise(function (resolve, reject) {
        setTimeout(function () {
            for (const arg of args) {
                if (typeof arg !== 'number') {
                    reject(`Invalid argument: ${arg}`);
                }
                total += arg;
            }
            resolve(total);
        }, 500);
    });
}

console.time('codeExecution');
sum(1, 3, 5).then(function (a) {
    console.log(a);
    return sum(2, 4).then(function (b) {
        console.log(b);
        return sum(a, b).then(function (result) {
            console.log(result);
            console.timeEnd('codeExecution');
        });
    });
}).catch(function (error) {
    console.log(error);
});

But, when I add an invalid argument to the sum() function, i.e. not a number, the reject() method runs, but it don't stop the then() chain, as should be, and we also get an exception. This can be seen from the following code:

import _Promise from "./Promise.mjs";

function sum(...args) {
    let total = 0;
    return new _Promise(function (resolve, reject) {
        setTimeout(function () {
            for (const arg of args) {
                if (typeof arg !== 'number') {
                    reject(`Invalid argument: ${arg}`);
                }
                total += arg;
            }
            resolve(total);
        }, 500);
    });
}

console.time('codeExecution');
sum(1, 3, '5').then(function (a) {
    console.log(a);
    return sum(2, 4).then(function (b) {
        console.log(b);
        return sum(a, b).then(function (result) {
            console.log(result);
            console.timeEnd('codeExecution');
        });
    });
}).catch(function (error) {
    console.log(error);
});

Also, if I catch an error in nested then() methods, the outer catch() doesn't notice this and I get an exception again. The goal is to implement a lightweight functional version of Promises, but not necessarily with all its functionality. Could you help me?

cj-2307
  • 259
  • 3
  • 14
  • What difficulties are you having, what would you like help with? You've narrowed it down to the catch method, but in regards to that, what is the problem? – Cory Harper Sep 14 '21 at 23:36
  • the code you present in your question runs - perhaps if you show code that fails you may get an answer – Bravo Sep 14 '21 at 23:51
  • I think the issue is when we try to add a string into the sum function (sum(1, 3, 5,'foo'), we don't break the then()-chain. and we also throw an `Unhandled Promise rejection` when we shouldn't. Just guessing though. – John Sep 14 '21 at 23:54
  • Sorry guys, I will edit the question to make my problem clearer. – cj-2307 Sep 14 '21 at 23:58
  • 2
    Really, the issue is that this is a naive implementation of Promises - no offence intended, but even the smallest proper implementation of Promises is twice the size of this. The first issue is, `.then` can be called multiple times on a promise. Secondly, `.then` takes two arguments, `onResolved` and `onRejected` - the question asks "How to implement a custom Promise" - however, the code is nothing like a Promise - https://promisesaplus.com/ – Bravo Sep 14 '21 at 23:58
  • 2
    Maybe these readings can be helpful - https://stackoverflow.com/questions/23772801/basic-javascript-promise-implementation-attempt/23785244#23785244 - https://www.promisejs.org/implementing/ - https://github.com/kriskowal/q/blob/v1/design/README.md – John Sep 15 '21 at 00:04

2 Answers2

2

The problem in your code is that your sum function calls both the reject and the resolve functions. There's no handling in the sum function that will cause it not to call the resolve function at the end, and there is nothing in your _Promise that blocks this behavior.

You have 2 options to fix this.

Option 1 would be if you want your _Promise to act like a real Promise you will need to manage a state and once a promise got to a final state stop calling the callback or errorCallback.

Option 2 would be to prevent from calling both reject and resolve in the function calling the _Promise, in this case, the sum function.

Gal Talmor
  • 1,004
  • 9
  • 14
0

With the comments that you guys provide me, I was able to improve the code and correct the errors mentioned, as shown below. Now, I would like you to give me suggestions on how to proceed and improve the code. Thanks. (The code can also be found on github).

const PENDING = 0;
const FULFILLED = 1;
const REJECTED = 2;

function _Promise(executor) {

    let state = PENDING;
    let callOnFulfilled = [];
    let callOnRejected = undefined;;

    function resolve(...args) {
        if (!state) {
            state = FULFILLED;
        }

        resolveCallbacks(...args);
    };
    function reject(error) {
        state = REJECTED;
        if (callOnRejected && (callOnRejected instanceof Function)) {
            callOnRejected(error);
            callOnRejected = undefined;
            callOnFulfilled = [];
        } else {
            throw `Unhandled Promise Rejection\n\tError: ${error}`;
        }
    };
    function resolveCallbacks(...value) {
        if (state !== REJECTED) {
            let callback = undefined;
            do {
                callback = callOnFulfilled.shift();
                if (callback && (callback instanceof Function)) {
                    const result = callback(...value);
                    if (result instanceof _Promise) {
                        result.then(resolveCallbacks, reject);
                        return;
                    } else {
                        value = [result];
                    }
                }
            } while (callback);
        }
    };

    if (executor && (executor instanceof Function)) {
        executor(resolve, reject);
    }

    this.then = function (onFulfilled, onRejected) {
        if (onFulfilled) {
            callOnFulfilled.push(onFulfilled);
            if (state === FULFILLED) {
                resolveCallbacks();
            }
        }
        if (onRejected && !callOnRejected) {
            callOnRejected = onRejected;
        }
        return this;
    };
    this.catch = function (onRejected) {
        return this.then(undefined, onRejected);
    };
}

function sum(...args) {
    let total = 0;
    return new _Promise(function (resolve, reject) {
        setTimeout(function () {
            for (const arg of args) {
                if (typeof arg !== 'number') {
                    reject(`Invalid argument: ${arg}`);
                }
                total += arg;
            }
            resolve(total);
        }, 500);
    });
}

console.time('codeExecution');
sum(1, 3, 5).then(function (a) {
    console.log(a);
    return sum(2, 4).then(function (b) {
        console.log(b);
        return sum(a, b).then(function (result) {
            console.log(result);
            return 25;
        });
    }).then(function (value) {
        console.log(value);
        console.timeEnd('codeExecution');
    });
}).catch(function (error) {
    console.log(error);
});
cj-2307
  • 259
  • 3
  • 14
  • 1
    You can try running your code against the [test suite](https://github.com/promises-aplus/promises-tests) of https://promisesaplus.com. There is still a lot to be desired. – Bergi Sep 15 '21 at 21:08