Understanding Promises by writing your own promise library

Dark Jedi
7 min readAug 29, 2018

Promise results in computation of asynchronous work which may have succeeded/failed. It makes the asynchronous code look like synchronous. One can chain promises to perform a series of computation.

Promise concept evolved in early 80’s and 90’s . It came around in JavaScript recently few years back. It was popularized by jQuery [5].

Why Promises ?

JavaScript is single threaded and all the events that happen in the browser like painting, updating styles and handling user actions are queued. One of these can delay the others in the queue. Everyone might have already used events and callbacks [6]. For those who haven’t can have a look at the example given below. Assume we have a single button which on clicking might change to a random color.

<body>
<button>Change color</button>
</body>
<script>
let btn = document.querySelector('button')
const random = number => Math.floor(Math.random()*(number+1))
btn.onclick = () => {
let rndCol = `rgb(${random(255)},${random(255)},${random(255)})`
document.body.style.backgroundColor = rndCol
}
</script>

Here we store the reference to the button using document.querySelector function. We also have a function expression random which will give us a random number. And lastly we have the event handler for onclick event which creates a random color and sets the background color of the body to it.

Callback vs Promises

Now assume an asynchronous operation like fetching data from some database like mongoDB or reading a file from a directory.

// A simple API in express to get all tasks from database using callbacks// index.js
app.get('/tasks', (req, res) => {
db.getAllTasks(function (err, data) {
if (err) {
res.json({
'status': 'error',
'message': err.message
})
} else {
res.json({
'status': 'success',
'data': data
})
}
})
})
// db.js
const getAllTasks = cb => {
db.collection('TaskCollection')
.find({type: 'task'})
.toArray((err, value) => {
if (err) {
cb(err)
} else {
cb(null, value)
}
})
}

Now the same code using promises.

// A simple API in express to get all tasks from database using promises.// index.js
app.get('/tasks', (req, res) => {
db.getAllTasks()
.then(data => res.json({ 'status': 'success','data': data }))
.catch(err => res.json({ 'status': 'error', 'message': err.message }))
// db.js
const getAllTasks = () => db.collection('TaskCollection')
.find({type: 'task'})
.toArray()

Using promises the code looks much more cleaner and easy to read. Now just imagine if you want to make multiple calls one after the other or maybe together or you want to read multiple files from a directory. The code starts becoming really messy and hard to read. Thus promise provides a simpler and concise interface to do asynchronous impure operations which involve some side effect.

Digging deeper into promises…
The promise can be understood as a state machine which has 3 states: Pending, Fulfilled and Rejected. When pending, a promise can transition to fulfilled or rejected state. In fulfilled and rejected state, a promise cannot transition to any other state and must have a value or reason for rejection.

A promise takes a callback with resolve and reject as parameters. Promise constructor will have states pending, fulfilled and rejected as well as promise value.

const states = {
0: 'pending',
1: 'fulfilled',
2: 'rejected'
}
function MyPromise (cb) {
if (typeof cb!== 'function') {
throw new TypeError('callback must be a function')
}
let state = states[0]
let value = null
}

Now we can add the fulfill and reject functions for our promise which will set the state and value for the promise.

const states = {
0: 'pending',
1: 'fulfilled',
2: 'rejected'
}
function MyPromise (cb) {
if (typeof cb!== 'function') {
throw new TypeError('callback must be a function')
}
let state = states[0]
let value = null

function fulfill (result) {
state = states[1]
value = result
}
function reject (error) {
state = states[2]
value = error
}
}

Now a basic state machine is complete. It has two functions to move from pending state to fulfilled or rejected state. Now we need another function that will help us in transitioning to fulfilled or rejected depending on the computation. The resolve function will take a value or a promise and try to extract then from a promise and bind the value to then and wait for the promise to resolve or it will fulfill the promise by passing the value passed in resolve to fulfill function.

const states = {
0: 'pending',
1: 'fulfilled',
2: 'rejected'
}
function MyPromise (cb) {
if (typeof cb!== 'function') {
throw new TypeError('callback must be a function')
}
let state = states[0]
let value = null

function fulfill (result) {
state = states[1]
value = result
}
function reject (error) {
state = states[2]
value = error
}
function resolve (value) {
try {
let then = getThen(value)
if (then) {
resolveAnotherPromise(then.bind(value), resolve, reject)
return
}
fulfill(value)
} catch (err) {
reject(err)
}
}
}

The getThen function takes a value or promise and returns then method (in case of promise) or null if it’s not a promise. resolveAnotherPromise will help in resolving a value which was passed to resolve function. It passes the resolve and reject function to the callback passed in constructor.

const states = {
0: 'pending',
1: 'fulfilled',
2: 'rejected'
}
function MyPromise (cb) {
if (typeof cb!== 'function') {
throw new TypeError('callback must be a function')
}
let state = states[0]
let value = null

function fulfill (result) {
state = states[1]
value = result
}
function reject (error) {
state = states[2]
value = error
}
function resolve (value) {
try {
let then = getThen(value)
if (then) {
resolveAnotherPromise(then.bind(value), resolve, reject)
return
}
fulfill(value)
} catch (err) {
reject(err)
}
}
}
function getThen (value) {
if (typeof(value) === 'object'
|| typeof(value) === 'function') {
let then = value.then
if (typeof(then) === 'function') return then
}
return null
}

function resolveAnotherPromise (cb, Onfulfill, Onreject) {
let finished = false
try {
cb(value => {
if (finished) return
finished = true
Onfulfill(value)
}, reason => {
if (finished) return
finished = true
Onreject(reason)
})
} catch (err) {
if (finished) return
finished = true
Onreject(err)
}
}
}

Now we add handler for collecting .then and .catch in an array. And call handle from done function to add .then or .catch on the next tick. Our fulfill and reject functions will call them on success or failure. A different approach to it can be, setting up an event emitter and emit events for .then or .catch. The event based approach would be really helpful for a lazy promise.

const states = {
0: 'pending',
1: 'fulfilled',
2: 'rejected'
}
function MyPromise (cb) {
if (typeof cb!== 'function') {
throw new TypeError('callback must be a function')
}
let state = states[0]
let value = null
let handlers = []
function fulfill (result) {
state = states[1]
value = result
handlers.forEach(handle)
handlers = null
} function reject (error) {
state = states[2]
value = error
handlers.forEach(handle)
handlers = null
}

function resolve (value) {
try {
let then = getThen(value)
if (then) {
resolveAnotherPromise(then.bind(value), resolve, reject)
return
}
fulfill(value)
} catch (err) {
reject(err)
}
}
function handle (handler) {
if (state === states[0]) handlers.push(handler)
else {
if (state === states[1] &&
typeof handler.onFulfill === 'function') {
handler.onFulfill(value)
}
if (state === states[2] &&
typeof handler.onReject === 'function') {
handler.onReject(value)
}
}
}
this.done = function (onFulfill, onReject) {
setTimeout(() => handle(onFulfill, onReject), 0)
}
}
function getThen (value) {
if (typeof(value) === 'object'
|| typeof(value) === 'function') {
let then = value.then
if (typeof(then) === 'function') return then
}
return null
}

function resolveAnotherPromise (cb, Onfulfill, Onreject) {
let finished = false
try {
cb(value => {
if (finished) return
finished = true
Onfulfill(value)
}, reason => {
if (finished) return
finished = true
Onreject(reason)
})
} catch (err) {
if (finished) return
finished = true
Onreject(err)
}
}
}

Now we can add .then function for creating new promises and chaining them. It internally call’s our .done function with 2 functions as parameter, which will call handle on the next tick of event loop and check if state is pending the push the functions into the array handlers or else execute them from the value/error-reason obtained.

const states = {
0: 'pending',
1: 'fulfilled',
2: 'rejected'
}
function MyPromise (cb) {
if (typeof cb!== 'function') {
throw new TypeError('callback must be a function')
}
let state = states[0]
let value = null
let handlers = []
function fulfill (result) {
state = states[1]
value = result
handlers.forEach(handle)
handlers = null
}function reject (error) {
state = states[2]
value = error
handlers.forEach(handle)
handlers = null
}
function resolve (value) {
try {
let then = getThen(value)
if (then) {
resolveAnotherPromise(then.bind(value), resolve, reject)
return
}
fulfill(value)
} catch (err) {
reject(err)
}
}
function handle (handler) {
if (state === states[0]) handlers.push(handler)
else {
if (state === states[1] &&
typeof handler.onFulfill === 'function') {
handler.onFulfill(value)
}
if (state === states[2] &&
typeof handler.onReject === 'function') {
handler.onReject(value)
}
}
}
this.done = function (onFulfill, onReject) {
setTimeout(() => handle(onFulfill, onReject), 0)
}
this.then = function (onFulfill, onReject) {
let self = this
return new Promise((resolve, reject) => {
return self.done(result => {
if (typeof onFulfill === 'function') {
try {
return resolve(onFulfill(result))
} catch (err) {
return reject(err)
}
} else {
return resolve(result)
}
}, error => {
if (typeof onReject === 'function') {
try {
return resolve(onReject(error))
} catch (err) {
return reject(err)
}
} else {
return reject(error)
}
})
})
}
}
function getThen (value) {
if (typeof(value) === 'object'
|| typeof(value) === 'function') {
let then = value.then
if (typeof(then) === 'function') return then
}
return null
}

function resolveAnotherPromise (cb, Onfulfill, Onreject) {
let finished = false
try {
cb(value => {
if (finished) return
finished = true
Onfulfill(value)
}, reason => {
if (finished) return
finished = true
Onreject(reason)
})
} catch (err) {
if (finished) return
finished = true
Onreject(err)
}
}
}

The working code for this promise library [4].

Interesting things to implement in promises can be cancellable and lazy promise. If you want to get even deeper into designing, refer Kriskowal design decisions [7].

Special Thanks to Forbes Lindesay for this phenomenal answer on stack overflow about promises [3].

Reference:

[1.] https://promisesaplus.com/

[2.] https://www.promisejs.org/implementing/

[3.] https://stackoverflow.com/questions/23772801/basic-javascript-promise-implementation-attempt/23785244#23785244

[4.] https://github.com/AkshayIyer12/promisesLib

[5.] https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-promise-27fc71e77261

[6.] https://developers.google.com/web/fundamentals/primers/promises

[7.] https://github.com/kriskowal/q/tree/master/design

--

--

Dark Jedi

From here and there in the vicinity of the cosmos