- The Frontend Feed
- Posts
- Implementing a Custom JavaScript Promise from Scratch
Implementing a Custom JavaScript Promise from Scratch
Before diving into this custom implementation, you may want to read our comprehensive guide on JavaScript Promises to understand the basics.
JavaScript promises are essential for handling asynchronous operations in a more readable and maintainable way than traditional callbacks. Understanding how to build a custom promise from scratch not only deepens your knowledge of promises but also equips you with a better understanding of asynchronous programming. In this guide, we’ll walk through how to create a CustomPromise class, explaining each step and providing insights into what’s happening behind the scenes.
Step 1: Setting Up the Basic Structure
We start by defining the CustomPromise class. A promise has three states: Pending, Fulfilled, and Rejected. We’ll also need to store the callbacks that will be executed when the promise is resolved or rejected.
const STATE = {
FULFILLED: "fulfilled",
REJECTED: "rejected",
PENDING: "pending",
};
class CustomPromise {
#thenCbs = [];
#catchCbs = [];
#state = STATE.PENDING;
#value;
constructor(callback) {
try {
callback(this.#onSuccess.bind(this), this.#onFail.bind(this));
} catch (e) {
this.#onFail(e);
}
}
}
Explanation:
State Management: The promise starts in the PENDING state and can transition to FULFILLED or REJECTED.
Callback Storage: We use arrays (#thenCbs and #catchCbs) to store .then() and .catch() callbacks.
Constructor: The constructor accepts a callback function that is immediately invoked with two arguments: resolve and reject (bound to #onSuccess and #onFail methods).
Step 2: Handling Fulfillment and Rejection
Next, we define how the promise handles success (fulfillment) and failure (rejection). We’ll create private methods #onSuccess and #onFail to manage these states.
#onSuccess(value) {
if (this.#state !== STATE.PENDING) return;
if (value instanceof CustomPromise) {
value.then(this.#onSuccess.bind(this), this.#onFail.bind(this));
return;
}
this.#value = value;
this.#state = STATE.FULFILLED;
this.#runCallbacks();
}
#onFail(value) {
if (this.#state !== STATE.PENDING) return;
if (value instanceof CustomPromise) {
value.then(this.#onSuccess.bind(this), this.#onFail.bind(this));
return;
}
this.#value = value;
this.#state = STATE.REJECTED;
this.#runCallbacks();
}
Explanation:
State Check: Both methods check if the promise is still PENDING before transitioning to another state.
Handling Nested Promises: If the value is another CustomPromise, it waits for that promise to settle before proceeding.
State Transition: If the state transitions to FULFILLED or REJECTED, the #runCallbacks method is called to execute the appropriate callbacks.
Step 3: Running Callbacks
The #runCallbacks method ensures that all registered callbacks are executed asynchronously after the promise settles.
#runCallbacks() {
queueMicrotask(() => {
if (this.#state === STATE.FULFILLED) {
this.#thenCbs.forEach(callback => callback(this.#value));
this.#thenCbs = [];
}
if (this.#state === STATE.REJECTED) {
this.#catchCbs.forEach(callback => callback(this.#value));
this.#catchCbs = [];
}
});
}
Explanation:
Microtask Queue: The queueMicrotask function ensures that the callbacks are executed asynchronously after the current execution context.
Callback Execution: The method iterates through the callbacks stored in #thenCbs or #catchCbs and executes them with the stored value.
Step 4: Implementing .then(), .catch(), and .finally()
Finally, we implement the public methods .then(), .catch(), and .finally() that allow users to interact with the promise.
then(thenCb, catchCb) {
return new CustomPromise((resolve, reject) => {
this.#thenCbs.push(result => {
if (!thenCb) {
resolve(result);
return;
}
try {
resolve(thenCb(result));
} catch (error) {
reject(error);
}
});
this.#catchCbs.push(result => {
if (!catchCb) {
reject(result);
return;
}
try {
resolve(catchCb(result));
} catch (error) {
reject(error);
}
});
this.#runCallbacks();
});
}
catch(cb) {
return this.then(undefined, cb);
}
finally(cb) {
return this.then(
result => {
cb();
return result;
},
result => {
cb();
throw result;
}
);
}
Explanation:
.then() Method: Registers callbacks for fulfillment and rejection, allowing you to handle the resolved value or any error. When .then() is called, a new CustomPromise is returned, enabling promise chaining for further operations. If an error occurs within the then callback, it is passed down the chain to the next .catch() or rejection handler.
.catch() Method: Serves as a convenient way to handle errors that occur in the promise chain. It is essentially a shorthand for .then(undefined, cb), where the catch callback processes any rejection or error from the promise. This allows you to isolate error handling logic while keeping the promise chain intact.
.finally() Method: Executes a callback regardless of whether the promise was fulfilled or rejected. It is useful for cleanup actions that need to occur no matter the outcome. The callback in .finally() does not alter the value or error, ensuring the promise chain continues unaffected after cleanup.
Step 5: Handling Uncaught Errors
To handle cases where no .catch() is provided, we throw a custom error to make debugging easier.
class UncaughtPromiseError extends Error {
constructor(error) {
super(error);
this.stack = `(in promise) ${error.stack}`;
}
}
Explanation:
Custom Error: If a promise is rejected and no .catch() is present, the UncaughtPromiseError is thrown, providing a clear stack trace for debugging.
By following these steps, you’ve implemented a custom CustomPromise class that closely mimics the behavior of native JavaScript promises. This exercise not only helps you understand promises more deeply but also prepares you for technical interviews where you might be asked to explain or implement a promise from scratch.
Final Custom Promise Implementation Code with sample example:
const STATE = {
FULFILLED: "fulfilled",
REJECTED: "rejected",
PENDING: "pending",
};
class CustomPromise {
#thenCbs = [];
#catchCbs = [];
#state = STATE.PENDING;
#value;
constructor(cb) {
try {
cb(this.#onSuccess.bind(this), this.#onFail.bind(this));
} catch (e) {
this.#onFail(e);
}
}
#onSuccess(value) {
if (this.#state !== STATE.PENDING) return;
if (value instanceof CustomPromise) {
value.then(this.#onSuccess.bind(this), this.#onFail.bind(this));
return;
}
this.#value = value;
this.#state = STATE.FULFILLED;
this.#runCallbacks();
}
#onFail(value) {
if (this.#state !== STATE.PENDING) return;
if (value instanceof CustomPromise) {
value.then(this.#onSuccess.bind(this), this.#onFail.bind(this));
return;
}
this.#value = value;
this.#state = STATE.REJECTED;
this.#runCallbacks();
}
#runCallbacks() {
queueMicrotask(() => {
if (this.#state === STATE.FULFILLED) {
this.#thenCbs.forEach(callback => callback(this.#value));
this.#thenCbs = [];
}
if (this.#state === STATE.REJECTED) {
this.#catchCbs.forEach(callback => callback(this.#value));
this.#catchCbs = [];
}
});
}
then(thenCb, catchCb) {
return new CustomPromise((resolve, reject) => {
this.#thenCbs.push(result => {
if (!thenCb) {
resolve(result);
return;
}
try {
resolve(thenCb(result));
} catch (error) {
reject(error);
}
});
this.#catchCbs.push(result => {
if (!catchCb) {
reject(result);
return;
}
try {
resolve(catchCb(result));
} catch (error) {
reject(error);
}
});
this.#runCallbacks();
});
}
catch(cb) {
return this.then(undefined, cb);
}
finally(cb) {
return this.then(
result => {
cb();
return result;
},
result => {
cb();
throw result;
}
);
}
}
class UncaughtPromiseError extends Error {
constructor(error) {
super(error);
this.stack = `(in promise) ${error.stack}`;
}
}
// Usage example
const testPromise = new CustomPromise((resolve, reject) => {
setTimeout(() => {
resolve("Success!");
}, 1000);
});
testPromise
.then(value => {
console.log(value);
return "Another success!";
})
.then(value => {
console.log(value);
})
.catch(error => {
console.error(error);
})
.finally(() => {
console.log("Operation complete");
});
Further Reading
🚀 If you enjoyed this post, why not show your love for coding with our Javascript Developer Themed Tees from Usha Creations? Discover the perfect shirt for your next hackathon!
Reply