Implement Custom Promise from scratch using vanilla javascript
This is another most commonly asked front-end interview question in most of the top companies. I myself have asked this question many times when I interviewed people in Amazon.
This is a fairly straightforward question, to be honest, and if you are a good javascript developer, you should answer this question fairly easily.
(Reference - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
Let's break down the problem statement:
We are asked to implement a custom version of
Promise
class so that by doing let's sayconst customPromise = new CustomPromise((resolve, reject) => {});
, you should be able to get the functionalities of the actualPromise
.Ok, obviously in a given short time of the interview, we can't implement everything that a promise does. We can hence check beforehand what are the things the interviewer is expecting to be done. This is critical as the interviewer would expect the candidates to clarify their thought process with them regardless of whether they are correct or not. Let's say that we implement the following things of the
Promise
functionality:Actual Promise constructor implementation
Handing
.then
Handling
.catch
Handling
.finally
Let's start by building one use case at a time.
Actual Promise implementation
class CustomPromise {
// executableMethod takes the resolve, reject arguments as its parameters.
constructor(exectableMethod) {}
}
// Setting some defaults.
CustomPromise.prototype.state = "pending";
CustomPromise.prototype.result = undefined;
export default CustomPromise;
We have a simple structure to get started. We have the constructor
that takes the method and we also set some defaults for state
and result
. In the actual promise, they would be represented as Internal Slots
(Reference: https://v8.dev/blog/understanding-ecmascript-part-1#internal-slots-and-internal-methods)
Ok, we know that Promise
constructor takes the following structure:
constructor((resolve, reject) => { })
(or)
if done using ES5, function Promise(function (resolve, reject) {}) {}
We need a way to access the arguments (resolve and reject) that are passed inside the method. It's a bit tricky as we can't do it directly. arguments
keyword would only return the method and not its arguments. Bind/Call
comes to our rescue.
constructor(exectableMethod) {
if (typeof exectableMethod !== "function") {
throw new Error("function is not provided in the arguments. exiting.");
}
function resolve(data) {}
function reject(err) {}
// This is critical as we need to bind the `resolve`
// and the `reject` methods to our internal methods
// for us to be able to access whenever the promise
// is either resolved or rejected from the code by the user.
exectableMethod.call(null, resolve, reject);
}
Let's leave this right here and move on to .then
implementation.
Handling .then
We have the promise in the following structure:
const promise = new Promise((resolve, reject) => {
...
});
promise.then((data) => {
// Do something with the data.
});
If you take a closer look, the .then
function takes a method as an argument, let's call that as then callback. That method gets invoked whenever the promise is resolved
. In order for us to invoke that method, we need to store that method in memory.
// Takes the callback methods of `.then`
thenQueue = [];
then(thenCallback) {
this.thenQueue.push(thenCallback);
return this; // This is important to support chaining
}
function resolve(data) {
this.state = "fulfilled";
this.result = data;
let value = data;
this.thenQueue.forEach((method) => {
if (typeof method === "function") {
value = method.call(null, value);
}
});
}
In this resolve
method, we look for all the chained .then
and call each of them with the value returned from the previous then. Also, we change the status of the promise to fullfilled
and the result to data
Handling .catch
Handling .catch
is pretty simple just like above. There is no .catch
chaining so just one method call.
// Catch callback
catchCallBack = null;
function reject(err) {
this.state = "fulfilled";
if (typeof this.catchCallback === "function") {
this.catchCallback.call(null, err);
}
handleFinally.call(this);
}
catch(catchCallback) {
this.catchCallback = catchCallback;
return this;
}
Handling .finally
.finally
gets called regardless of whether the promise is resolve/rejected.
// Finally callback
finallyCallback = null;
function resolve(data) {
this.state = "fulfilled";
this.result = data;
let value = data;
this.thenQueue.forEach((method) => {
if (typeof method === "function") {
value = method.call(null, value);
}
});
handleFinally(); // Add this line to call the finally
}
function reject(err) {
this.state = "fulfilled";
if (typeof this.catchCallBack === "function") {
this.catchCallBack.call(null, err);
}
handleFinally(); // Add this line to call the finally
}
function handleFinally() {
if (typeof this.finallyCallback === "function") {
this.finallyCallback.call(null);
}
}
finally(finallyCallback) {
this.finallyCallback = finallyCallback;
}
You can find the complete source code here - https://gist.github.com/thearunkumar/2bf8288a82b0f71d9f971afb79c070b5
You can use the following snippet to test the above code:
const customPromise = new CustomPromise((res, rej) => {
setTimeout(() => {
res("Hey");
}, 2000);
});
customPromise
.then((data) => {
console.log("First then", data);
})
.then((data) => {
console.log("Second then", data);
})
.then((data) => {
console.log("Third then", data);
})
.catch((err) => {
console.log("Catch", err);
})
.finally(() => {
console.log("Finally");
});
If you like this article, you can check out my other articles as well and subscribe to the newsletter to get my latest article right in your inbox.
Cheers
Arunkumar Sri Sailapathi.