3

I'm trying to implement a simple promise class with chainable .then() functionality in javascript. Here's what I've done till now -

class APromise {
    constructor(Fn) {
        this.value = null;
        Fn(resolved => { this.value = resolved; });
    }
    then(fn) {
        fn(this.value);
        return this;
    }
}

function myFun() {
    return new APromise(resolve => {
        // just resolve('Hello'); works but this doesn't
        setTimeout(() => { resolve('Hello'); }, 2000);
    });
}

const log = v => { console.log(v); };

myFun().then(log).then(log);

This outputs -

null
null

Instead of 'Hello' 2 times. I think it is currently ignoring setTimeout() call, how should I make this work?

hg_git
  • 2,884
  • 6
  • 24
  • 43
  • You were returning an already defined method `.then()` hence that minor error about `undefined`. – Abhinav Gauniyal Oct 05 '17 at 02:59
  • 1
    I've seen another question just like this a few days ago. First mistake is that `.then()` must return a new promise that is chained to the original. Here's the other question: https://stackoverflow.com/questions/46530696/promise-from-scratch – jfriend00 Oct 05 '17 at 03:06
  • 1
    Also, you don't seem to understand what `.then()` does. It just stores a callback that is called sometime in the future when the promise gets resolved (and also returns a new promise that is chained to the original). You have to store a list of resolve callbacks and a list of reject callbacks. Then, when the promise is resolved or rejected, you call all the callbacks of the right type. Implementing all the promise logic is much more involved than this. – jfriend00 Oct 05 '17 at 03:08
  • We aren't going to write this code for you so you need to ask a much more specific question as basically right now, your logic is entirely wrong and shows that you don't yet understand what promises do or how they work. You should probably start by studying the spec in much greater detail and then learn how to actually write code that uses chained and nested promises so you can see what they really do. Or go study some of the simpler implementations that already exist (references are in comments to the other question I previously linked to). – jfriend00 Oct 05 '17 at 03:11
  • @jfriend00 I think you misunderstood my question, I'm not trying to implement the 'promise' of javascript but I'm just trying to see how it works. This is just an exercise to understand more about promises :) – hg_git Oct 05 '17 at 03:14
  • Also it works currently when you don't call any kind of async code, ie replace setTimeout with just resolve call. I want to know how to make it work with async code :/ – hg_git Oct 05 '17 at 03:14
  • Uhh, you have to implement it like the standard, not the way you are doing so. Your best best is to go look at the several other simple implementations referenced in comments in that other question I linked above. We're not going to reiterate to you here how promises work. That's fully described in the spec and in hundreds of articles and I've already told you the first thing you need to do in my comment above. The fact that your code doesn't work with async operations means it's completely wrong. `.then()` never calls the callback you pass it immediately. Never. – jfriend00 Oct 05 '17 at 03:17
  • So, what is your actual question if you're not asking us to fix your code? – jfriend00 Oct 05 '17 at 03:17
  • 1
    Possible duplicate of [Concept - Distilling how a promise works?](https://stackoverflow.com/questions/15668075/concept-distilling-how-a-promise-works) – hg_git Oct 05 '17 at 03:53
  • Noteworthy: [Understanding the Promise/A+ spec](https://stackoverflow.com/a/36192729/6445533) –  Oct 05 '17 at 06:25

3 Answers3

7

The problem

Your code isn't working the way you want because you are mixing async flow with sync flow.

When you call .then(), it will return this synchronously. Since setTimeout() is an async function that is called after some time (2 secs), this.value is still null.

If you want to know more about the asynchronous flow of JS, I recommend watching this video. It's a bit long but super helpful.


Making your code work

Since we can't tell when setTimeout() will call the function passed to it, we cannot call and callback functions that depend on its operations. We store these callbacks in an array for later consumption.

When the setTimeout() function is called (promise resolves), we have the result of promise resolution. And so, we now call all the bound callbacks.

class APromise {
    constructor(Fn) {
        this.value = null;
-       Fn(resolved => { this.value = resolved; });
+       this.callbacks = [];
+       Fn(resolved => {
+           this.value = resolved;
+
+           this.callbacks.forEach(cb => {
+               cb(this.value);
+           });
+       });
    }
    then(fn) {
-       fn(this.value);
+       this.callbacks.push(fn);
        return this;
    }
}

function myFun() {
    return new APromise(resolve => {
        setTimeout(() => { resolve('Hello'); }, 2000);
    });
}

const log = v => { console.log(v); };

myFun().then(log).then(log);

The chaining problem

The code above solves the problem partially.

True chaining is achieved when the result of one callback is passed on to the next. That's not the case in our current code. TO make that happen, each .then(cb) must return a new APromise that resolves when the cb function is called.

A complete and Promises/A+ conformant implementation is way over the scope of a single SO answer, but that shouldn't give the impression that it isn't doable. Here's a curated list of custom implentations.


Fuller implementation

Let's start with a clean slate. We need a class Promise that implements a method then which also returns a promise to allow chaining.

class Promise {
    constructor(main) {
        // ...
    }

    then(cb) {
        // ...
    }
}

Here, main is a function that takes a function as an argument and calls it when the promise is resolved/fulfilled - we call this method resolve(). The said function resolve() is implemented and provided by our Promise class.

function main(resolve) {
    // ...
    resolve(/* resolve value */);
}

The basic feature of the then() method is to trigger/activate the provided callback function cb() with the promise value, once the promise fulfills.

Taking these 2 things into account, we can rewire our Promise class.

class Promise {
    constructor(main) {
        this.value = undefined;
        this.callbacks = [];

        const resolve = resolveValue => {
            this.value = resolveValue;

            this.triggerCallbacks();
        };

        main(resolve);
    }

    then(cb) {
        this.callbacks.push(cb);
    }

    triggerCallbacks() {
        this.callbacks.forEach(cb => {
            cb(this.value);
        });
    }
}

We can test our current code with a tester() function.

(function tester() {
    const p = new Promise(resolve => {
        setTimeout(() => resolve(123), 1000);
    });

    const p1 = p.then(x => console.log(x));
    const p2 = p.then(x => setTimeout(() => console.log(x), 1000));
})();

// 123 <delayed by 1 second>
// 123 <delayed by 1 more second>

This concludes our base. We can now implement chaining. The biggest problem we face is the the then() method must return a promise synchronously which will be resolved asynchronously.

We need to wait for the parent promise to resolve before we can resolve the next promise. This implies that instead of adding cb() to parent promise, we must add the resolve() method of next promise which uses return value of cb() as its resolveValue.

then(cb) {
-   this.callbacks.push(cb);
+   const next = new Promise(resolve => {
+       this.callbacks.push(x => resolve(cb(x)));
+   });
+
+   return next;
}

If this last bit confuses you, here are some pointers:

  • Promise constructor takes in a function main() as an argument
  • main() takes a function resolve() as an argument
    • resolve() is provided by the Promise constructor
  • resolve() takes an argument of any type as the resolveValue

Demo

class Promise {
    constructor(main) {
        this.value = undefined;
        this.callbacks = [];

        const resolve = resolveValue => {
            this.value = resolveValue;

            this.triggerCallbacks();
        };

        main(resolve);
    }

    then(cb) {
        const next = new Promise(resolve => {
            this.callbacks.push(x => resolve(cb(x)));
        });

        return next;
    }

    triggerCallbacks() {
        this.callbacks.forEach(cb => {
            cb(this.value);
        });
    }
}

(function tester() {
    const p = new Promise(resolve => {
        setTimeout(() => resolve(123), 1000);
    });

    const p1 = p.then(x => console.log(x));
    const p2 = p.then(x => setTimeout(() => console.log(x), 1000));
    const p3 = p2.then(x => setTimeout(() => console.log(x), 100));
    const p4 = p.then((x) => new Promise(resolve => {
        setTimeout(() => resolve(x), 1000);
    }))

    /*
        p: resolve after (1s) with resolveValue = 123
        p1: resolve after (0s) after p resolved with resolveValue = undefined
        p2: resolve after (0s) after p resolved with resolveValue = timeoutID
        p3: resolve after (0s) after p2 resolved with resolveValue = timeoutID
        p4: resolve after (1s) after p resolved with resolveValue = Promise instance
    */
})();

// 123  <delayed by 1s>
// 2    <delayed by 1.1s>
// 123  <delayed by 2s>
zhirzh
  • 3,273
  • 3
  • 25
  • 30
  • 1
    Lots of things missing here. For starters: 1) Doesn't handle reject. 2) Doesn't handle chaining (as you mention), 3) Doesn't always call callbacks on next tick (e.g. never synchronously), 4) Doesn't do anything with return values from `.then()` handlers which is another way of chaining or rejecting, 5) Doesn't handle exceptions in either the `Fn` function or in `.then()` handlers. – jfriend00 Oct 05 '17 at 04:32
  • 1
    as @hg_git said, *not to implement the 'promise' of javascript but to see how it works*. The problem in the code wasn't the lacklustre implementation, but the implicit disconnect between async and sync flow of JS. My answer covers, and is limited to, that part only. – zhirzh Oct 05 '17 at 04:40
  • @jfriend00, I hear you. so here's the solution: https://promisesaplus.com/implementations. A robust implementation is way over the scope of a single SO answer, but that shouldn't give the impression that it isn't doable. – zhirzh Oct 05 '17 at 04:42
  • Just making sure the OP has an idea what's missing which is at least as important in understanding promises as what is here and providing info for anyone who comes later so nobody confuses this with anything more than it is. And, I already provided the OP with a pointer to several links to other beginning implementations. This question has been answered several other times. Not criticizing you for showing this code, but this is not the most effective way to learn promises. – jfriend00 Oct 05 '17 at 04:42
  • The OP really needs to understand how promises work before trying to write an implementation. One has to understand what `.then()` does before trying to write it. Lots of great articles, doc, specs and code from other implementations that would be a better way to study and learn. – jfriend00 Oct 05 '17 at 04:47
  • 1
    @jfriend00 yes I understand your concerns but I would like to take incremental approach and learn from it. I'm not a spec implementor and just want to understand the inner workings of promises atm and I'm really helpful to zhirzh who took out his time to understand my problem and help me proceed forward :) – hg_git Oct 05 '17 at 05:05
  • @zhirzh would you mind giving a quick example of true chaining approach . Like what would user write instead of `myFun().then(log).then(log);` where I would need to return a new promise each time `.then()` is called? – hg_git Oct 05 '17 at 05:08
  • would making one of the function called inside `.then()` return a promise sufficient? – hg_git Oct 05 '17 at 05:15
0

Solve it, when call sync in Promise:

class MyPromise{
    constructor(fn){
        this.callback= null;
        this.data= null;
        this.calledInNext= false;
        fn((data, state)=>{ // unsafe when call resolve({}, 'string')
            this.calledInNext= (state === 'CALLED_IN_NEXT') ? true : false;
            this.data= data;
            if(this.callback) this.callback(this.data);
        }, function(_err){
            console.log('resolve({error}) to without catch')
        })
    }
    then(cb){ // next
        if(this.data || this.calledInNext){
            return new MyPromise(r => {
                r(cb(this.data), 'CALLED_IN_NEXT');
            });
        } else {
            return new MyPromise(r => {
                this.callback = x=> r(cb(x))
            })       
        }
    }
}

Or chain:

class MyPromise{
    constructor(fn){
        this.callbacks= [];
        this.data= null;
        fn((data)=>{
            this.data= data;
            var gg= this.data;
            this.callbacks.forEach(el=>{
                gg= el(gg);
            })
        })
    }
    then(cb){
        if(this.data || this._calledInNext){
            this._calledInNext= true; this.data= cb(this.data); return this;
        } else {
            this.callbacks.push(cb); return this;
        }
    }
}

Test:

(new MyPromise(function(resolve, reject){
    // setTimeout(resolve, 1000, {done: 1})
    resolve({done: 1})
})).then(data=>{
    console.log(data);      // {done: 1}
    return data;
}).then(data=>{
    console.log(data);      // {done: 1}
    return {};
}).then(data=>{
    console.log(data);      // {}
}).then(data=>{
    console.log(data);      // undefine
}).then(data=>{
    console.log(data);      // undefine
}).then(data=>{
    console.log(data);      // undefine
})
Diep Gepa
  • 515
  • 5
  • 5
-1

I wrote a simple custom promise myself

class Promise {
  constructor(main) {
    this.main = main;
    this.mainExecuted = false;
    this.resolved = false;
    this.rejected = false;
    this.promiseChain = [];
    this.handleError = () => {};
    this.onResolve = this.onResolve.bind(this);
    this.onReject = this.onReject.bind(this);
  }

  then(handleSuccess) {    
    if (this.resolved) {
        if(!this.rejected) {
        this.args = handleSuccess(this.args)
      }
    }
    else {
      this.promiseChain.push(handleSuccess);
      this.main(this.onResolve, this.onReject);
      this.thenExecuted = true;
    }
    return this;
  }

  catch(handleError) {
    this.handleError = handleError;
    if(!this.mainExecuted) {
            this.main(this.onResolve, this.onReject);
        this.thenExecuted = true;
    }
    return this;
  }

  onResolve(...args) {
    this.resolved = true;
    this.args = args;
        try {
      this.promiseChain.forEach((nextFunction) => {
        this.args = nextFunction(...this.args);
      });
    } catch (error) {
      this.promiseChain = [];

      this.onReject(error);
    }
  }

  onReject(error) {
    this.rejected = true;
    this.handleError(error);
  }
}

Implementing for old zombie browsers (ES3 to above)

I was using Promise that figured out in old browsers this class doesn't exist. So that I implemented one for them that have resolve and reject methods that is transpiled using babel:

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
var Promise = /*#__PURE__*/function () {
  "use strict";

  function Promise(main) {
    _classCallCheck(this, Promise);
    this.main = main;
    this.mainExecuted = false;
    this.resolved = false;
    this.rejected = false;
    this.promiseChain = [];
    this.handleError = function () {};
    this.onResolve = this.onResolve.bind(this);
    this.onReject = this.onReject.bind(this);
  }
  _createClass(Promise, [{
    key: "then",
    value: function then(handleSuccess) {
      if (this.resolved) {
        if (!this.rejected) {
          this.args = handleSuccess(this.args);
        }
      } else {
        this.promiseChain.push(handleSuccess);
        this.main(this.onResolve, this.onReject);
        this.thenExecuted = true;
      }
      return this;
    }
  }, {
    key: "catch",
    value: function _catch(handleError) {
      this.handleError = handleError;
      if (!this.mainExecuted) {
        this.main(this.onResolve, this.onReject);
        this.thenExecuted = true;
      }
      return this;
    }
  }, {
    key: "onResolve",
    value: function onResolve() {
      var _this = this;
      this.resolved = true;
      for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
        args[_key] = arguments[_key];
      }
      this.args = args;
      try {
        this.promiseChain.forEach(function (nextFunction) {
          _this.args = nextFunction.apply(void 0, _toConsumableArray(_this.args));
        });
      } catch (error) {
        this.promiseChain = [];
        this.onReject(error);
      }
    }
  }, {
    key: "onReject",
    value: function onReject(error) {
      this.rejected = true;
      this.handleError(error);
    }
  }]);
  return Promise;
}();
Amir Fo
  • 5,163
  • 1
  • 43
  • 51