Ever wondered how JavaScript Promises work under the hood? In this blog, we’ll walk through how to implement a Promise from scratch — step-by-step, with simple explanations. By the end, you’ll not only understand Promises deeply, but also how chaining, error handling, and cleanup work.
What is a Promise?
A Promise is a JavaScript object that represents the eventual completion or failureof an asynchronous operation and its resulting value.
A Promise can be in one of three states:
- Pending – The initial state.
- Fulfilled – The operation completed successfully.
- Rejected – The operation failed.
Step 1: Basic Custom Promise Implementation
Let’s build a class called MyPromise
with basic functionality.
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(value));
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback(reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
if (this.state === 'fulfilled') {
onFulfilled(this.value);
} else if (this.state === 'rejected') {
onRejected(this.reason);
} else {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
}
}
What Each Part Does
state
,value
, andreason
store the Promise status and result.resolve()
transitions state to'fulfilled'
and triggers success callbacks.reject()
transitions to'rejected'
and triggers failure callbacks.then()
allows users to attach.then()
callbacks.
Step 2: Add Chaining and Error Handling
We now enhance our then()
method to support chaining and error handling, like .then().then().catch()
.
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(cb => cb());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(cb => cb());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
} else if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
} else {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
}
Step 3: Implement resolvePromise
Helper Function
function resolvePromise(promise, x, resolve, reject) {
if (promise === x) {
return reject(new TypeError('Chaining cycle detected'));
}
let called = false;
if (x instanceof MyPromise) {
x.then(
value => {
if (!called) {
called = true;
resolvePromise(promise, value, resolve, reject);
}
},
reason => {
if (!called) {
called = true;
reject(reason);
}
}
);
} else if (x && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then;
if (typeof then === 'function') {
then.call(
x,
y => {
if (!called) {
called = true;
resolvePromise(promise, y, resolve, reject);
}
},
r => {
if (!called) {
called = true;
reject(r);
}
}
);
} else {
resolve(x);
}
} catch (err) {
if (!called) {
called = true;
reject(err);
}
}
} else {
resolve(x);
}
}
Step 4: Add finally()
and Static Helpers
MyPromise.prototype.finally = function(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value),
reason => MyPromise.resolve(callback()).then(() => { throw reason; })
);
};
MyPromise.resolve = function(value) {
return new MyPromise(resolve => resolve(value));
};
MyPromise.reject = function(reason) {
return new MyPromise((_, reject) => reject(reason));
};
Usage Example
new MyPromise((resolve, reject) => {
setTimeout(() => resolve("Step 1 done"), 1000);
})
.then(res => {
console.log(res);
return "Step 2";
})
.then(res => {
console.log(res);
throw new Error("Something went wrong");
})
.catch(err => {
console.error("Caught error:", err.message);
})
.finally(() => {
console.log("Cleaned up!");
});
Alternative Ways to Handle Async Logic
1. Callback Pattern (Old School)
doSomething((err, result) => {
if (err) {
handleError(err);
} else {
doAnother(result, (err2, res2) => {
// Callback Hell!
});
}
});
2. Async/Await (Modern)
async function runTasks() {
try {
const res1 = await doStep1();
const res2 = await doStep2(res1);
console.log(res2);
} catch (err) {
console.error("Error:", err);
}
}
Why Build Custom Promises?
- You deeply understand async patterns
- You learn what’s happening behind
then()
,catch()
, andawait
- It sharpens your problem-solving and debugging skills
Resources
Final Words
Now that you’ve implemented your own version of JavaScript Promises, you have a powerful new mental model for async JavaScript. You’re not just using promises — you understand how they work from the inside out.
Happy coding!