Before promises, we handled asynchronous code with callbacks which were hard to handle well. besides being an unreadable code (pyramid of doom), trying to handle multiple callbacks to run after an asynchronous operation ended or handle a completed process later in the code was hard to handle. There was a need for a better coding approach for handling asynchronous events.
Side Note: I recommend that you will read my previous article named “Asynchronous JavaScript”.
A promise is a standardized approach to dealing with asynchronous events and callbacks. It means that promises are not something new that JavaScript could not do before, but it is a standard that is available in JavaScript, we don’t have to write it ourselves, and everyone uses the same object.
A promise is an object that represents a future value. A value that we know eventually we going to get but we might not have yet.
Let’s try to create our own implementation of a promise to get the ideas around promises and after that, we will use the implementation of promises in JavaScript to understand more.
Custom Promise
The promise represents a value that comeback after work is completed so there are 3 states that the promise can be in:
- Work is pending
- Work is completed
- Work could not be complete
const PENDING = 0;
const FULLFILLED = 1;
const REJECTED = 2;
After defining the states, let’s create the shell of the promise object:
const PENDING = 0;
const FULLFILLED = 1;
const REJECTED = 2;
function PolyfillPromise(executor) {
}
As you can see we created a function with a capital letter that indicated for us that it will be a constructor function that will be invoked with the “new” keyword which will create for us an object, a promise object in our case.
We pass to the constructor a function (executor) that will actually do the work. the promise doesn’t do any of the work, the promise only wait for the work to be complete and than figuring what to do after that. we as a developer should write the work in the executor function and pass it to the promise and the promise will run it.
const PENDING = 0;
const FULLFILLED = 1;
const REJECTED = 2;
function PolyfillPromise(executor) {
// current state
let state = PENDING;
// the value that we waitng for - starts as null
let value = null;
// we might want more then one handeling functions to run when work is complete (chaining "thens")
let handlers = [];
// we might want more then one handelin functions to run when the work could not be done (cahining "catches")
let catches = []
// creating a function that will be called when the work is done and we have a result
function resolve(result) {
if (state !== PENDING) return;
state = FULLFILLD;
// we set the internal value to whatever result the executor function gave us
value = result;
// now we will run all the handlers and give them the value that just came back
handlers.forEach((handle) => handle(value));
}
// creating a function that will be called when the work is done and we have a result
function reject(err) {
if (state !== PENDING) return;
state = REJECTED;
// we set the internal value to whatever result the executor function gave us
value = err;
// now we will run all the handlers and give them the value that just came back
catches.forEach((catch) => catch(err));
}
// we creating a method on the promise object that the constructor will create
this.then = function(callback) {
// when we call ".then" when the promise already resolved, we will:
if(state === FULLFILLED) {
// ... execute the callback imidiattly with the value we already have
callback(value);
} else {
// ... if we still waiting, we will add the callback to the array of callbackthat we going to execute when its done
handlers.push(callback);
}
// the same with "this.then" but handeling the errors
this.catch= function(callback) {
// when we call ".catch" when the promise already rejected, we will:
if(state === REJECTED) {
// ... execute the callback imidiattly with the value we already have
callback(value);
} else {
// ... if we still waiting, we will add the callback to the array of callbackts that we going to execute when its rejected
catches.push(callback);
}
}
// after the promise is created we will run the executor function that we pass to the promise.
// the promise represents a process that already running, so when we create the promise, it startsthe work
// the executor function expect a reolve and reject functions.
executor(resolve, reject);
}
The resolve function and reject function will be called by the executor function and the “then” and “catch” methods will be used with the promise object.
Now, let’s use our promise. we will create an executor function and a promise object, and pass the executor to the promise:
const doWork = (res, rej) => {
setTimeout(() => { res("Big Success") }, 3000);
}
let promiseResult = new PolyfillPromise(doWork);
// the function we pass here to the "the" is the function that is passed to the handlers array.
promiseResult.then((result) => {
console.log("The result is: " + result);
});
// We can do it again...
promiseResult.then((result) => {
console.log("Show result again: " + result);
});
// We can even add another handler after the promise already resolved.
setTimeout(() => {
// We can do it again...
promiseResult.then((result) => {
console.log("Here we go again: " + result);
});
}, 10000);
Native Promise
The “then” method in native promise can return a promise which is a great feature because if the callback that we pass to “then” also does something that requires us to wait for returned value, we can chain a sequence of “then”, and by that, we can flatten the pyramid of doom.
let’s see what that means by using a native promise. the below code demonstrates the same use of the promise as above but this time we use the native Promise instead the custom promise we created above:
const doWork = (res, rej) => {
setTimeout(() => { res("Big Success") }, 3000);
}
let promiseResult = new Promise(doWork);
promiseResult.then((result) => {
console.log("The result is: " + result);
});
As I mentioned above, the “then” method can return a promise. it means that if the handling function returns a value the return will be a promise that eventually will be the value. check the code below:
const doWork = (res, rej) => {
setTimeout(() => { res("Big Success") }, 3000);
}
let promiseResult = new Promise(doWork);
let anotherPromiseResult = promiseResult
.then((result) => {
console.log("The result is: " + result); // The result is: Big Success
return "Another Big Success";
});
anotherPromiseResult.then((result) => {
console.log("The result is: " + result); // The result is: The result is: Another Big Success
});
// With the JavaScript chaining ability we can write it differently.
promiseResult
.then((result) => {
console.log("The result is: " + result); // The result is: Big Success
return "Another Big Success";
})
.then((result) => {
console.log("The result is: " + result); // The result is: The result is: Another Big Success
});
The 2 handlers that are being passed to the 2 “then” methods are not being attached to the original promise that we created with the “new” keyword, only the first one and the second handler is attached to the new promise that is returned from the “then” method.
Notice that even when we return a string from the handler in the first “then” method, the “then” method will return a promise. and if that’s not cool enough, we can return a new creation of a promise like this:
const doWork = (res, rej) => {
setTimeout(() => { res("Big Success") }, 3000);
}
const doWork2 = (res, rej) => {
setTimeout(() => { res("Big Success Again") }, 5000);
}
let promiseResult = new Promise(doWork);
promiseResult
.then((result) => {
console.log("The result is: " + result); // Big Success (after 3 sec)
return new Promise(doWork2);
})
.then((result) => {
console.log("The result is: " + result); // Big Success Again (after total of 8 sec)
});
We made asynchronous work as a result of the first asynchronous work, which means that after the first doWork function completes (after the first promise is resolved), the second function doWork2 will run.
The handler that returns a promise is a parameter of the “then” method that always returns a promise. the promise that the “then” return in this case is synced up with the promise we return in the handler. it means that when the returned promise resolves, the promise of the “then” resolves and will have the same value as the returned promise resolves to. it’s as we added “.then” to the returned promise like this: “return new Promise(doWork2).then();”, but it’s not exactly the same because it’s adding it to a new promise created by the “.then” method.
in a nutshell, inside of every “.then” we can have a new asynchronous process that returns a new promise and the promise object will make sure that each “.then” will only run after the promise resolves. this will give us a sequance that works properly without getting into a pyramid of doom.