JavaScript Promises From Scratch: A Beginner’s Walkthrough for Technical Interviews


Demystifying a Custom Promise in JavaScript
Why build your own promise?
Before we dive in, let’s remember that native Promises in JavaScript help us handle asynchronous work—things like fetching data from a server or waiting for a timer. But implementing your own is a fantastic exercise to understand how Promises really work under the hood.
🔗 Prerequisites
If any of these concepts are brand‑new to you, take a quick detour through the MDN docs before proceeding:
The Big Picture
A CustomePromise
has three main pieces:
State tracking (
PENDING
→FULFILLED
orREJECTED
)Queuing up callbacks (the functions you pass into
.then()
)Executing those callbacks once the promise settles
By the end of this post, you’ll see how it all fits together.
1. Defining the States
const STATES = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECT: "rejected"
}
We start by giving our promise three possible moods:
Pending: nothing’s happened yet
Fulfilled: it succeeded
Rejected: it failed
2. The Constructor
class CustomePromise {
#handleExecutor = [];
#state = STATES.PENDING;
#value = "";
constructor(callback) {
try {
callback(this.#resolve, this.#reject); // executors obtains the result either resolve and reject
this.#handleUpdates(this.#state, this.#value);
} catch (error) {
this.#reject(error);
}
}
// …
}
We accept an executor function (just like native
new Promise((resolve, reject) => {…})
).Immediately call it with our own
#resolve
and#reject
methods.We wrap it in
try…catch
so any synchronous error goes to rejection.
3. Resolving & Rejecting
#resolve = (data) => {
this.#handleUpdates(STATES.FULFILLED, data);
}
#reject = (error) => {
this.#handleUpdates(STATES.REJECT, error);
}
Both these methods simply hand off to a shared helper, #handleUpdates
, telling it the new state and value.
4. Handling Updates & Simulating Async
#handleUpdates = (state, value) => {
if (this.#state !== STATES.PENDING) return;
// as we know then() method handle async data return value and return it...
// here to mimic the async operation we just used the setTimeout
setTimeout(() => {
if (value instanceof CustomePromise) {
// if the value is instance of CustomePromise then we need to wait for it to resolve
// this will wait for the value to resolve and then return the value
// this will also handle the case where the value is a promise
// and we need to wait for it to resolve before returning it
value.then();
}
// these fields are used to track the state of promise
// and the value that is returned from the executor function
// this will be used to track the state of promise and return the value to then() method
this.#value = value;
this.#state = state;
console.log("state ", this.#state, " value ", this.#value);
this.#handleExcutor(); // this will handle and call all the executor functions that are pushed in the array
}, 1000);
};
Only run once (we bail out if not pending).
Use
setTimeout
to ensure.then()
callbacks never fire synchronously.Support chaining promises: if you
resolve( anotherPromise )
, we wait for it.
5. Queuing Handlers
Every time you do .then(onSuccess, onFailure)
, you want to remember those two callbacks. We do that here:
#pushHandler = (executor) => {
this.#handleExecutor.push(executor);
this.#handleExcutor(); // this to support late .then()
}
We don’t invoke them immediately, because the promise might still be pending.
6. Running the Queue
Once we settle, we loop through every queued handler:
#handleExcutor = () => {
if (this.#state === STATES.PENDING) return;
// this will handle the case where the promise is resolved or rejected
if (this.#state === STATES.FULFILLED) {
this.#handleExecutor.forEach((executor) => {
executor.onSucess(this.#value);
});
} else if (this.#state === STATES.REJECT) {
// this will handle the case where the promise is rejected
// this will call the onFailure method of the executor function that is pushed in the array
// this will handle the case where the promise is rejected and return the value to then() method
// this will also handle the case where the value is a promise and we need to wait for it to resolve before returning it
this.#handleExecutor.forEach((executor) => {
executor.onFailure(this.#value);
});
}
// reset the handleExecutor array to avoid memory leak
this.#handleExecutor = [];
this.#state = STATES.PENDING; // reset the state to pending to allow new promise to be created
this.#value = ""; // reset the value to empty string to allow new promise to be created
};
If fulfilled, call each handler’s
onSucess(value)
.If rejected, call each handler’s
onFailure(value)
.Then clear out everything so a new promise can start fresh.
7. The .then()
Method
Here’s where we return a brand‑new CustomePromise
, enabling chaining:
then = (onSucess, onFailure) => {
return new CustomePromise((resolve, reject) => {
const handleSuccess = {
onSucess: (value) => {
if (!onSucess) return resolve(value);
try {
const result = onSucess(value);
if (result instanceof CustomePromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
},
onFailure: (error) => {
if (!onFailure) return reject(error);
try {
const result = onFailure(error);
if (result instanceof CustomePromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
},
};
this.#pushHandler(handleSuccess);
});
};
We wrap your handlers in a
wrapper
that:Calls your
onSuccess
/onFailure
if providedCatches exceptions and rejects accordingly
If you return another promise, we chain it
8. The .catch()
Shortcut
catch = (onError) => {
return this.then(null, onError);
}
Exactly like native promises, .catch(fn)
is just .then(undefined, fn)
under the hood.
Putting It All Together
// using # infront of methods or fieldname
const STATES = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECT: "rejected",
};
class CustomePromise {
#handleExecutor = [];
#state = STATES.PENDING;
#value = "";
constructor(callback) {
try {
callback(this.#resolve, this.#reject); // callback function i.e. executors functions which accept two params resolve and reject
this.#handleUpdates(this.#state, this.#value);
} catch (error) {
this.#reject(error);
}
}
#resolve = (data) => {
this.#handleUpdates(STATES.FULFILLED, data);
};
#reject = (error) => {
// console.error(error)
this.#handleUpdates(STATES.REJECT, error);
};
// this method handle whatever the updated value we get from callback function (i.e. either resolve or reject...)
#handleUpdates = (state, value) => {
if (this.#state !== STATES.PENDING) return;
// as we know then() method handle async data return value and return it...
// here to mimic the async operation we just used the setTimeout
setTimeout(() => {
if (value instanceof CustomePromise) {
// if the value is instance of CustomePromise then we need to wait for it to resolve
// this will wait for the value to resolve and then return the value
// this will also handle the case where the value is a promise
// and we need to wait for it to resolve before returning it
value.then();
}
// these fields are used to track the state of promise
// and the value that is returned from the executor function
// this will be used to track the state of promise and return the value to then() method
this.#value = value;
this.#state = state;
this.#handleExcutor(); // this will handle and call all the executor functions that are pushed in the array
}, 1000);
};
// main executor function that handle tboth resolve and reject cases
#pushHandler = (executor) => {
this.#handleExecutor.push(executor);
this.#handleExcutor(); // this to support late .then()
};
#handleExcutor = () => {
if (this.#state === STATES.PENDING) return;
// this will handle the case where the promise is resolved or rejected
if (this.#state === STATES.FULFILLED) {
this.#handleExecutor.forEach((executor) => {
executor.onSucess(this.#value);
});
} else if (this.#state === STATES.REJECT) {
// this will handle the case where the promise is rejected
// this will call the onFailure method of the executor function that is pushed in the array
// this will handle the case where the promise is rejected and return the value to then() method
// this will also handle the case where the value is a promise and we need to wait for it to resolve before returning it
this.#handleExecutor.forEach((executor) => {
executor.onFailure(this.#value);
});
}
// reset the handleExecutor array to avoid memory leak
this.#handleExecutor = [];
this.#state = STATES.PENDING; // reset the state to pending to allow new promise to be created
this.#value = ""; // reset the value to empty string to allow new promise to be created
};
then = (onSucess, onFailure) => {
return new CustomePromise((resolve, reject) => {
const handleSuccess = {
onSucess: (value) => {
if (!onSucess) return resolve(value);
try {
const result = onSucess(value);
// support promise chaining
if (result instanceof CustomePromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
},
onFailure: (error) => {
if (!onFailure) return reject(error);
try {
const result = onFailure(error);
if (result instanceof CustomePromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
},
};
this.#pushHandler(handleSuccess);
});
};
catch = (onError) => {
this.then(null, onError);
};
}
const customePromise = new CustomePromise((res, rej) => {
res("Done!")
})
.then(msg => {
console.log(msg)
})
.catch(err => console.error("Oops:", err));
// Output -> Done
Executor runs immediately, schedules a 500 ms callback.
After 500 ms,
#resolve("Done!")
→#handleUpdates
→ queue flush.First
.then()
handler logs “Done!” and returns"Next step"
.That value gets passed to the next
.then()
, logging “Next step”.
Why Build a Custom Promise?
Deepens understanding: You see how state, queues, and chaining work.
Hands‑on experience with async patterns.
Appreciation for the polished behavior of native
Promise
.
I hope this guide makes custom Promises click for you! Feel free to implement by yourself with this reference, you will definetly learn tons along the way.
I'd love to hear what you think!
Leave a comment below or connect with me on GitHub / LinkedIn
Subscribe to my newsletter
Read articles from Ronak Mathur directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
