Promise - under the hood
I have decided to start back with how it works series of articles. Here is the first of many to come. I am pretty sure you are using/used the promise in Javascript many times, either knowingly or not. Let us see what is a Promise and how it works under the hood by writing a custom Promise function.
In Javascript, a Promise is an Object that is used to represent the eventual completion/failure of an asynchronous operation.
How does promise works?
- Promise has three states
pending
,fulfilled
, andrejected
. - Promise can either
resolve
orreject
only once. - Promise constructor returns a Promise object which is
thenable
andcatchable
. - Promise constructor accepts a callback as a parameter.
- Promise constructor callback has two parameter callbacks called
resolve()
andreject()
. - And there is a
finally()
method which is called when the promise issettled
.
Promise example code:
We will start with a simple example of a promise.
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 5000);
});
myPromise
.then((result) => {
console.log(result); // "1" after 5 seconds
})
.catch((error) => {
console.log(error);
})
.finally(() => {
console.log('Finally'); // Logs when promise is settled
});
The above code snippet is a simple example that is self-explanatory if not let me explain. After 5 seconds, the above code will resolve the myPromise constructor. and it will log 1 in the console and also it will log Finally. Simple right, I can resolve a promise with resolve callback and reject a promise with reject callback and finally is called when the promise is settled.
We are going to understand and write CustomPromise function in 4 steps.
Step 1:
- We are going to create a file and name it as customPromise.js.
- let us name the function as CustomPromise which takes a callback as a parameter.
- To call
.then()
,.catch()
and.finally()
methods, we need to add then, catch and finally methods in this keyword of function and each method should return this to make it chainable. - And invoke CustomPromise with new keyword so that it makes it chainable.
Example: customPromise.js file
function CustomPromise(callback) {
this.then = () => this;
this.catch = () => this;
this.finally = () => this;
}
const callbackFn = () => {};
const myPromise = new CustomPromise(callbackFn);
myPromise
.then(() => console.log)
.catch(() => console.log)
.finally(() => console.log);
Wow, we just learned how to create a CustomPromise function and how to make it chainable like an actual Promise object in Javascript.
Step 2:
- Promise allows us to attach as many .then() as possible. So we will create a internal array to store the callbacks passed to then() method called thenCallbacksArr.
- Also I should be able to attach .catch() method. So we will also store the callback in a internal variable called catchCallback.
- Lets create a resolve and reject callback function and pass it to the callback parameter of CustomPromise function.
function CustomPromise(callback) {
let thenCallbacksArr = []; // To hold as many then as possible
let catchCallback = null; // To hold catch callback
this.then = (myThenCallback) => {
thenCallbacksArr.push(myThenCallback); // Stores all then callbacks
return this;
};
this.catch = (myCatchCallback) => {
catchCallback = myCatchCallback;
return this;
};
this.finally = () => {
return this;
};
function resolve() {}
function reject() {}
callback(resolve, reject);
}
Points to remember:
- Promise only calls the first
catch()
method and discards the other catch methods. finally()
method doesn't accept any parameters.
Step 3:
- Create a variable called promiseState with the initial value as pending.
- Create another Boolean variable as called to track whether a resolve or reject callback is called or not.
- When resolve or reject callback is called, pass the then or catch error argument accordingly.
- Once the promise is settled, Check promiseState and called variables before calling
finally()
method.
function CustomPromise(callback) {
let thenCallbacksArr = []; // To hold as many then as possible
let catchCallback = null; // To hold catch callback
let promiseState = 'pending'; // To keep track of promise state (resolved/rejected)
let called = false; // To know if resolve/reject is called or not
this.then = (myThenCallback) => {
thenCallbacksArr.push(myThenCallback); // Stores all then callbacks
return this;
};
this.catch = (myCatchCallback) => {
catchCallback = myCatchCallback;
return this;
};
this.finally = (myFinallyCallback) => {
// Post resolve or reject, we will call the finally method
if (called && promiseState !== 'pending') {
myFinallyCallback();
}
return this;
};
function resolve(arg) {
promiseState = 'resolved';
// If resolve/reject not called
if (!called) {
called = true;
// We need to iterate thenCallbackArr and pass prev result to then callback
thenCallbacksArr.reduce((result, cb) => cb(result), arg);
}
}
function reject(error) {
promiseState = 'rejected';
// If reject/resolve not called
if (!called) {
called = true;
// Catch is optionally so check if its present
if (typeof catchCallback === 'function') {
catchCallback(error);
}
}
}
callback(resolve, reject);
}
Above CustomPromise function will work just fine but when a promise is resolved/rejected before we can assign a then or catch or finally method, there is no code to handle that. That's what we will do in step 4.
Step 4:
- We need to check promiseState, called variable, and call the thenCallbacksArr which contains all the then callbacks.
- Also we need to push the finally callback to thenCallbackArr variable, for it to call when a promise is settled before attaching the finally method.
function CustomPromise(callback) {
let thenCallbacksArr = []; // To hold as many then as possible
let catchCallback = null; // To hold catch callback
let promiseState = 'pending'; // To keep track of promise state (resolved/rejected)
let called = false; // To know if resolve/reject is called or not
let resolveArguments = null; // To hold resolve arguments
let rejectArguments = null; // To hold reject error
this.then = (myThenCallback) => {
thenCallbacksArr.push(myThenCallback); // Stores all then callbacks
// If resolve callback method is already called, but then was not atached
if (called && promiseState === 'resolved') {
// Since there can ba many then's, call the first and store the returned result
// and pass resolveArguments to it
var firstThenFunzInArr = thenCallbacksArr.shift();
resolveArguments = firstThenFunzInArr(resolveArguments);
}
return this;
};
this.catch = (myCatchCallback) => {
catchCallback = myCatchCallback;
// If reject callback method is already called, but catch was not atached
if (called && promiseState === 'rejected') {
catchCallback(rejectArguments);
}
return this;
};
this.finally = (myFinallyCallback) => {
thenCallbacksArr.push(myFinallyCallback);
// Post resolve or reject, we will call the finally method
if (called && promiseState !== 'pending') {
myFinallyCallback();
}
return this;
};
function resolve(arg) {
promiseState = 'resolved';
// If resolve/reject not called
if (!called) {
called = true;
resolveArguments = arg;
// We need to iterate thenCallbackArr and pass prev result to then callback
thenCallbacksArr.reduce((result, cb) => cb(result), arg);
}
}
function reject(error) {
promiseState = 'rejected';
// If reject/resolve not called
if (!called) {
called = true;
rejectArguments = error;
// Catch is optionally so check if its present
if (typeof catchCallback === 'function') {
catchCallback(error);
}
}
}
callback(resolve, reject);
}
Demo:
Final thoughts
If you followed through with the code, you would have understood now how a promise works internally. Also, this is purely for educational purposes only, and dont ask questions like "Write a polyfill for the promise" in interviews.
I would have also missed a few edge cases. But you get it right? How it works. Hope you liked this post.
If you did, do share it with your friends and colleagues.