Promises

    Abhimanyu.13 Nov, 2023

    Let's build Promise from scratch.

    My current understanding

    The use of promises is basically to add a function to javascript's task queue once a value has been obtained by a long-running process (that process is usually carried out in a different thread or other part of the os, like when we request some value over the network or from hard disk).

    1let p = new Promise((resolve,reject) => {
    2  try {
    3    const value = //obtain some value from a long running task.
    4    resolve(value)
    5  } catch (e) {
    6    reject(e)
    7  }
    8})

    Now p is container for the value we actually want. Later in our code, when we want to do something with that value we can do that like so:

    1p.then((val) => {
    2  // do something with this value
    3})

    what we've done above is basically we've added the function (given to .then as an argument) to javascript's task queue. The event loop will pick it up when it gets time and add it to the call stack to be executed.

    Breaking it down

    So a promise is container for a value. It's constructor takes in a function. That function gets two arguments - a resolve function and a reject function. A promise also has a state indicating whether it is pending, fulfilled (resolve has been called), or rejected (reject has been called).

    Each time we call .then on the promise we are adding a function to a list. When the promise is fulfilled we need to execute all the functions in that list.

    We'll implement the constructor first:

    main.js
    class MyPromise {
    constructor(func) {
    this.value = undefined
    this.state = "pending"
    this.fulfillmentTasks = []
    this.rejectionTasks = []
    this.resolve = this.resolve.bind(this)
    this.reject= this.reject.bind(this)
    if (func) {
    func(this.resolve,this.reject)
    }
    }
    resolve(value) {
    if (this.state !== "pending") return this
    this.state = "fulfilled"
    this.value = value
    this.rejectionTasks = []
    this.fulfillmentTasks.map(t => setTimeout(t, 0))
    this.fullfillmentTasks = []
    return this
    }
    reject(value) {
    if (this.state !== "pending") return this
    this.state = "rejected"

    Implementing .then

    main.js
    MyPromise.prototype.then = function(onFulfilled, onRejected) {
    switch (this.state) {
    case "pending": {
    this.fulfillmentTasks.push(() => onFulfilled(this.value))
    this.rejectionTasks.push(() => onRejected(this.value))
    break
    }
    case "fulfilled":
    setTimeout(() => onFulfilled(this.value), 0)
    break
    case "rejected":
    setTimeout(() => onRejected(this.value), 0)
    break
    }
    }


    const p = new MyPromise((res,rej) => {
    res(2)
    })

    p.then((val) => console.log(val))
    p.then((val) => console.log(val*2))
    p.then((val) => console.log(val*3))
    p.then((val) => console.log(val*4))

    Chaining .then callbacks:

    1const p = new Promise((res,rej) => {
    2  res(2)
    3})
    4
    5p.then((val) => val*2).then((val) => val*2).then((val) => val*2).then((val) => console.log(val))
    6
    7// should print 16

    For this we need to return a promise from .then function

    main.js
    MyPromise.prototype.then = function(onFulfilled, onRejected) {
    const newPromise = new MyPromise()

    const fullfillmentTask = () => {
    const newValue = onFulfilled(this.value)
    newPromise.resolve(newValue)
    }
    switch (this.state) {
    case "pending": {
    this.fulfillmentTasks.push(fullfillmentTask)
    this.rejectionTasks.push(() => onRejected(this.value))
    break
    }
    case "fulfilled":
    setTimeout(fullfillmentTask, 0)
    break
    case "rejected":
    setTimeout(() => onRejected(this.value), 0)
    break
    }
    return newPromise
    }


    const newp = new MyPromise((res,rej) => {
    res(2)

    Flattening the .then

    1const p = new MyPromise((res) => {
    2  const p1 = new MyPromise((r) => r(2))
    3  res(p1)
    4})
    5
    6p.then((val) => val.then(v =>  console.log(v)))
    7//prints 2
    8
    9// But what we want is :
    10p.then(val => console.log(val))
    11//prints 2
    12
    13//so in effect p "becomes" p1

    To handle the above scenario:

    • p needs to "become" p1
    • p's value is p1's value
    • p's state is p1's state
    • we can't call p.resolve() once p1 resolves
    flattening.js
    const imp = require("./main.js")
    const MyPromise = imp.MyPromise
    function isThenable(val) {
    return typeof val === "object" && val !== null && typeof val.then === "function"
    }

    MyPromise.prototype.fulfill = function(val) {
    if (isThenable(val)) return
    this.value = val
    this.state = "fulfilled"
    this.rejectionTasks = []
    this.fulfillmentTasks.map(t => setTimeout(t, 0))
    this.fullfillmentTasks = []
    }

    MyPromise.prototype.resolve = function(value) {
    if (this.settled) return this
    this.settled = true

    if (isThenable(value)) {
    value.then((v) => this.fulfill(v))
    } else {
    this.fulfill(value)
    }
    return this
    }