Write A Promise

I. Why do we need Promise - “callback hell”

  1. Asynchronous call is the core of JavaScript across browser and Node.js.

  2. JavsScript runtime thread use callback to execute result of asynchronous task handled by other threads (Ajax thread, Timer thread, Rendering Thread)

  3. Example below are callback hell , which is a headache for programmer. For

    1
    2
    3
    4
    5
    6
    7
    8
    // when the nesting level exceeds 3, people will get crazy!!
    getData(function(x){ // getData of x
    getMoreData(x, function(y){ // getMoreData of y, using x
    getMoreData(y, function(z){ // getMoreData of x, using y
    ...
    });
    });
    });
  4. Is there a way to flatten the nesting callbacks, in a chainable way? => Node

    • Consider the Node data structure, it contains child node in itself, similar to our requirement!!
    • Promise is such a stateful utility object, which can be chained to implement nested callbacks

II. Promise Usage

  1. executer:

    • execute asynchronous action, generating result

    • when create new Promise, need to pass in pre-defined executer

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // executer: a function with parameters: `resolve` and `reject`
      let p = new Promise( function(resolve, reject){
      ajaxGet( url, function(res){
      if(res.status == 200)
      resolve(res.data)
      else
      reject(res.error)
      })
      })
  2. callbacks in then:

    • onFulfilled & onRejected specify how to handle the result of asynchronous action

    • use case 1: multiple callbacks on a promise

      1
      2
      3
      4
      5
      // Assume executor will: resolve(1)
      let callback1 = function(res){ console.log("This is from callback1: " + res*1) }
      let callback2 = function(res){ console.log("This is from callback2: " + res*2)}
      p.then(callback1) // This is from callback1: 1
      p.then(callback2) // This is from callback1: 2
    • use case 2: chaining callbacks

      1
      2
      3
      4
      5
      // Assume executor will: resolve(1)
      let callback1 = function(res){ return res+1 }
      let callback2 = function(res){ return res+1 }
      let callback3 = function(res){ console.log("The final res will be 3: " + res) }
      p.then(callback1).then(callback1).then(callback3) // The final res will be 3: 3

III. Promise: stateful utility object

  1. “Stateful”

    • Promise is an object, its properties are stateful, ie. property value can change dynamially

    • state can only change in 2 ways: pending => fulfilled, pending => rejected

    • the cause of stateful is: asynchronous actions inside the executor. Once instantiated to promise object, the executor will be executed inside promise object, and will update state, value, consumers

      1
      2
      3
      4
      5
      6
      7
      function MyPromise( executor ){
      this.state = "pending" // keep record of the state update
      this.value = undefined // store the result of asynchronous action
      this.consumers = [] // place to store callbacks (actually child promises)

      executor(this.resolve.bind(this), this.reject.bind(this)) // run executor
      }
  2. resolve & reject: hooks to receive asynchronous result.

    • The asynchronous function executor , will pass its result to Promise using its hooks: resolve and reject
  3. then: hooks to receive passed in callbacks onFulfilled & onRejected

    • the callbacks are passed to Promise using then hook
    • Promise will take care of executing callback, when asynchronous result is ready
  4. How to it chainable

    • In fact, then() method can not only receive callbacks,
    • but also can return a new Promise, which can be chained by using another then, …., and so on
    • then() create a new Promise, then put the callbacks inside the newly created Promise, then put into the consumers array to consume the asynchronous result when its ready

VI. Write One!

  1. Promise state:

    There are only 2 state transitions allowed: from pending to fulfilled, and from pending to rejected. No other transition should be possible, and once a transition has been performed, the promise value (or rejection reason) should not change.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function MyPromise(executor) {
    this.state = 'pending';
    this.value = undefined;
    executor(this.resolve.bind(this), this.reject.bind(this));
    }

    // provide only two ways to transition
    MyPromise.prototype.resolve = function (value) {
    if (this.state !== 'pending') return; // cannot transition anymore
    this.state = 'fulfilled'; // can transition
    this.value = value; // must have a value
    }

    MyPromise.prototype.reject = function (reason) {
    if (this.state !== 'pending') return; // cannot transition anymore
    this.state = 'rejected'; // can transition
    this.value = reason; // must have a reason
    }
  2. then method: the core of chaining

    • [Note 1]: each then() will return a new Promise!

    • The then method is the core to Promise, which can be chained using then. It has two tasks:

      • receive callbacks, and store them for later use when asynchorous result is ready
      • create new promise, to make promise chainable
    • In fact, we put callbacks of the current promise, into the newly created child promises. Each callback will correspond a new Promise creation

    • This way, we can also easily consume the outcome of the callback(result), to resolve(outcome)for the child promises

    • [Note 2]:

      • child promise execute its parent’s callback ;
      • and resolve outcome of that callback, pass the outcome to promise created by next then
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // For each then(), create a new Promise to embody this callback pair
      MyPromise.prototype.then = function(onFulfilled, onRejected) {
      var consumer = new MyPromise(function () {});

      // ignore onFulfilled/onRejected if not a function
      consumer.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
      consumer.onRejected = typeof onRejected === 'function' ? onRejected : null;

      // put the consumer with parent's callbacks onto `this.consumers`
      this.consumers.push(consumer);

      // It's possible that the promise was already resolved,
      // so, try to consume the resolved result
      this.broadcast();

      // must return a promise to make it chainable
      // used by next potential `then()` in the chain
      return consumer;
      };
    • broadcast(): Consume the asynchorous result

      After the asynchronous action return a result, we need to

      • first, update the state to fulfilled or rejected

      • execute the each callback of onFulfilled, onRejected (in child promise) collected by then

      • onFulfilled, onRejected must be called asynchronously

        for example, new Promise(executor).then(callback), callback execution must happen after executor. Thus, we can correctly get result even if executor is synchorous

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      MyPromise.prototype.broadcast = function() {
      var promise = this;
      // only get called after promise is resolved
      if (this.state === 'pending') return;

      // callback must be executed by its corresponding promise
      var callbackName = this.state == 'fulfilled' ? 'onFulfilled' : 'onRejected';

      // when no callback specified in then(), still need to resolve/reject it
      var resolver = this.state == 'fulfilled' ? 'resolve' : 'reject';

      // onFulfilled/onRejected must be called asynchronously
      setTimeout(function() {
      // traverse in order & call only once, using `splice(0)` for deletion
      promise.consumers.splice(0).forEach(function(consumer) {
      try {
      var callback = consumer[callbackName];
      // if a functon, call callback as plain function
      // else, ignore callback
      if (callback) {
      // child promise resolve outcome of callback(value)
      consumer.resolve(callback(promise.value));
      } else {
      // child promise resolve value without callback
      consumer[resolver](promise.value);
      }
      } catch (e) {
      consumer.reject(e);
      };
      })
      }, 0 );
      }
    • [Note 2]: the current then can only handle chainable promise when callback is pure function; Cannot handle when callback return a new Promise.

  3. Make then can response another Promise

    This time, we need to modify the resolve method to be able to receive another Promise; At the same time, the original one we call it fulfill.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    // The Promise Resolution Procedure: will treat values that are thenables/promises
    // and will eventually call either fulfill or reject/throw.
    MyPromise.prototype.resolve = function(x) {
    var wasCalled, then;
    // 2.3.1
    if (this === x) {
    throw new TypeError('Circular reference: promise value is promise itself');
    }
    // 2.3.2
    if (x instanceof MyPromise) {
    // 2.3.2.1, 2.3.2.2, 2.3.2.3
    x.then(this.resolve.bind(this), this.reject.bind(this));
    } else if (x === Object(x)) { // 2.3.3
    try {
    // 2.3.3.1
    then = x.then;
    if (typeof then === 'function') {
    // 2.3.3.3
    then.call(x, function resolve(y) {
    // 2.3.3.3.3 don't allow multiple calls
    if (wasCalled) return;
    wasCalled = true;
    // 2.3.3.3.1 recurse
    this.resolve(y);
    }.bind(this), function reject(reasonY) {
    // 2.3.3.3.3 don't allow multiple calls
    if (wasCalled) return;
    wasCalled = true;
    // 2.3.3.3.2
    this.reject(reasonY);
    }.bind(this));
    } else {
    // 2.3.3.4
    this.fulfill(x);
    }
    } catch(e) {
    // 2.3.3.3.4.1 ignore if call was made
    if (wasCalled) return;
    // 2.3.3.2 or 2.3.3.3.4.2
    this.reject(e);
    }
    } else {
    // 2.3.4
    this.fulfill(x);
    }
    }

    // 2.1.1.1: provide only two ways to transition
    MyPromise.prototype.fulfill = function (value) {
    if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
    this.state = 'fulfilled'; // 2.1.1.1: can transition
    this.value = value; // 2.1.2.2: must have a value
    this.broadcast();
    }

V. Test Our Own Promise (stack overflow)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
function MyPromise(executor) {
this.state = 'pending';
this.value = undefined;
// A list of "clients" that need to be notified when a state
// change event occurs. These event-consumers are the promises
// that are returned by the calls to the `then` method.
this.consumers = [];
executor(this.resolve.bind(this), this.reject.bind(this));
}

// 2.1.1.1: provide only two ways to transition
MyPromise.prototype.fulfill = function (value) {
if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.state = 'fulfilled'; // 2.1.1.1: can transition
this.value = value; // 2.1.2.2: must have a value
this.broadcast();
}

MyPromise.prototype.reject = function (reason) {
if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.state = 'rejected'; // 2.1.1.1: can transition
this.value = reason; // 2.1.3.2: must have a reason
this.broadcast();
}

// A promise’s then method accepts two arguments:
MyPromise.prototype.then = function(onFulfilled, onRejected) {
var consumer = new MyPromise(function () {});
// 2.2.1.1 ignore onFulfilled if not a function
consumer.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
// 2.2.1.2 ignore onRejected if not a function
consumer.onRejected = typeof onRejected === 'function' ? onRejected : null;
// 2.2.6.1, 2.2.6.2: .then() may be called multiple times on the same promise
this.consumers.push(consumer);
// It might be that the promise was already resolved...
this.broadcast();
// 2.2.7: .then() must return a promise
return consumer;
};

MyPromise.prototype.broadcast = function() {
var promise = this;
// 2.2.2.1, 2.2.2.2, 2.2.3.1, 2.2.3.2 called after promise is resolved
if (this.state === 'pending') return;
// 2.2.6.1, 2.2.6.2 all respective callbacks must execute
var callbackName = this.state == 'fulfilled' ? 'onFulfilled' : 'onRejected';
var resolver = this.state == 'fulfilled' ? 'resolve' : 'reject';
// 2.2.4 onFulfilled/onRejected must be called asynchronously
setTimeout(function() {
// 2.2.6.1, 2.2.6.2 traverse in order, 2.2.2.3, 2.2.3.3 called only once
promise.consumers.splice(0).forEach(function(consumer) {
try {
var callback = consumer[callbackName];
// 2.2.1.1, 2.2.1.2 ignore callback if not a function, else
// 2.2.5 call callback as plain function without context
if (callback) {
// 2.2.7.1. execute the Promise Resolution Procedure:
consumer.resolve(callback(promise.value));
} else {
// 2.2.7.3 resolve in same way as current promise
consumer[resolver](promise.value);
}
} catch (e) {
// 2.2.7.2
consumer.reject(e);
};
})
});
};

// The Promise Resolution Procedure: will treat values that are thenables/promises
// and will eventually call either fulfill or reject/throw.
MyPromise.prototype.resolve = function(x) {
var wasCalled, then;
// 2.3.1
if (this === x) {
throw new TypeError('Circular reference: promise value is promise itself');
}
// 2.3.2
if (x instanceof MyPromise) {
// 2.3.2.1, 2.3.2.2, 2.3.2.3
x.then(this.resolve.bind(this), this.reject.bind(this));
} else if (x === Object(x)) { // 2.3.3
try {
// 2.3.3.1
then = x.then;
if (typeof then === 'function') {
// 2.3.3.3
then.call(x, function resolve(y) {
// 2.3.3.3.3 don't allow multiple calls
if (wasCalled) return;
wasCalled = true;
// 2.3.3.3.1 recurse
this.resolve(y);
}.bind(this), function reject(reasonY) {
// 2.3.3.3.3 don't allow multiple calls
if (wasCalled) return;
wasCalled = true;
// 2.3.3.3.2
this.reject(reasonY);
}.bind(this));
} else {
// 2.3.3.4
this.fulfill(x);
}
} catch(e) {
// 2.3.3.3.4.1 ignore if call was made
if (wasCalled) return;
// 2.3.3.2 or 2.3.3.3.4.2
this.reject(e);
}
} else {
// 2.3.4
this.fulfill(x);
}
}



// ------------------ TEST ------------------
let p0 = new MyPromise((resolve, reject)=>{
setTimeout(()=>{ resolve(0) },1000)
})

let p1 = p0.then((res)=>{
console.log("p1 get result from p0, value should be 0: " + res)
return res+1
})

let p2 = p0.then((res)=>{
console.log("p2 get result from p0, value should be 0: " + res)
return res+2
})

let p3 = p0.then((res)=>{
console.log("p3 get result from p0, value should be 0: " + res)
return res+3
})

console.log("This should be an array of 3 Promises: " + p0.consumers)

let p4 = p1.then(res=>{ console.log("p4 get result from p1, value should be 1: " + res)})
let p5 = p2.then(res=>{ console.log("p5 get result from p2, value should be 2: " + res)})
let p6 = p3.then(res=>{ console.log("p6 get result from p3, value should be 3: " + res)})

Credits to:

  1. https://stackoverflow.com/questions/23772801/basic-javascript-promise-implementation-attempt/42057900#42057900
  2. https://www.promisejs.org/implementing/
  3. https://javascript.info/promise-chaining
  4. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
  5. http://thecodebarbarian.com/write-your-own-node-js-promise-library-from-scratch.html