Building Your Own Promise: A Deep Dive into Asynchronous JavaScript
20 February 20254 min read
In modern JavaScript development, Promises provide an elegant solution for handling asynchronous operations. This article delves into manually implementing a Promise from scratch, covering essential features like state transitions, asynchronous execution, chaining, and additional methods such as catch
, resolve
, all
, and race
.
1. Understanding the Promise Skeleton
A Promise object must have:
- A constructor function accepting an
executor
callback. - Three states:
pending
,fulfilled
, andrejected
. - A
.then()
method to handle success and failure.
Basic Promise Structure
1 function MyPromise(executor) {2 this.state = 'pending';3 this.value = null;4 this.reason = null;56 const resolve = value => {7 this.value = value;8 };9 const reject = reason => {10 this.reason = reason;11 };1213 executor(resolve, reject);14 }1516 MyPromise.prototype.then = function(onFulfilled, onRejected) {17 onFulfilled(this.value);18 onRejected(this.reason);19 };
This implementation lacks state transitions and asynchronous behavior, which we will address next.
2. Handling State Changes
A Promise must:
- Transition from
pending
tofulfilled
orrejected
irreversibly. - Execute
onFulfilled
only if the state isfulfilled
, andonRejected
ifrejected
.
Improved Version
1 function MyPromise(executor) {2 this.state = 'pending';3 this.value = null;4 this.reason = null;56 const resolve = value => {7 if (this.state === 'pending') {8 this.value = value;9 this.state = 'fulfilled';10 }11 };1213 const reject = reason => {14 if (this.state === 'pending') {15 this.reason = reason;16 this.state = 'rejected';17 }18 };1920 executor(resolve, reject);21 }2223 MyPromise.prototype.then = function(onFulfilled, onRejected) {24 if (this.state === 'fulfilled') {25 onFulfilled(this.value);26 }27 if (this.state === 'rejected') {28 onRejected(this.reason);29 }30 };
3. Implementing Asynchronous Execution
To ensure .then()
executes asynchronously, we use setTimeout
:
1 function MyPromise(executor) {2 this.state = 'pending';3 this.value = null;4 this.reason = null;5 this.onFulfilledCallbacks = [];6 this.onRejectedCallbacks = [];78 const resolve = value => {9 setTimeout(() => {10 if (this.state === 'pending') {11 this.value = value;12 this.state = 'fulfilled';13 this.onFulfilledCallbacks.forEach(cb => cb(value));14 }15 });16 };1718 const reject = reason => {19 setTimeout(() => {20 if (this.state === 'pending') {21 this.reason = reason;22 this.state = 'rejected';23 this.onRejectedCallbacks.forEach(cb => cb(reason));24 }25 });26 };2728 executor(resolve, reject);29 }3031 MyPromise.prototype.then = function(onFulfilled, onRejected) {32 return new MyPromise((resolve, reject) => {33 if (this.state === 'fulfilled') {34 setTimeout(() => {35 try {36 const result = onFulfilled(this.value);37 resolve(result);38 } catch (error) {39 reject(error);40 }41 });42 }4344 if (this.state === 'rejected') {45 setTimeout(() => {46 try {47 const result = onRejected(this.reason);48 resolve(result);49 } catch (error) {50 reject(error);51 }52 });53 }5455 if (this.state === 'pending') {56 this.onFulfilledCallbacks.push(value => {57 try {58 const result = onFulfilled(value);59 resolve(result);60 } catch (error) {61 reject(error);62 }63 });6465 this.onRejectedCallbacks.push(reason => {66 try {67 const result = onRejected(reason);68 resolve(result);69 } catch (error) {70 reject(error);71 }72 });73 }74 });75 };
4. Implementing Promise Chaining
To enable promise chaining, we introduce the resolvePromise
function:
1 function resolvePromise(promise, result, resolve, reject) {2 if (result === promise) {3 return reject(new TypeError('Chaining cycle detected'));4 }56 if (result instanceof MyPromise) {7 result.then(resolve, reject);8 return;9 }1011 if (typeof result === 'object' || typeof result === 'function') {12 try {13 let then = result.then;14 if (typeof then === 'function') {15 then.call(result, resolve, reject);16 } else {17 resolve(result);18 }19 } catch (error) {20 reject(error);21 }22 } else {23 resolve(result);24 }25 }
Now, we modify .then()
to use resolvePromise
:
1 MyPromise.prototype.then = function(onFulfilled, onRejected) {2 return new MyPromise((resolve, reject) => {3 if (this.state === 'fulfilled') {4 setTimeout(() => {5 try {6 const result = onFulfilled(this.value);7 resolvePromise(this, result, resolve, reject);8 } catch (error) {9 reject(error);10 }11 });12 }1314 if (this.state === 'rejected') {15 setTimeout(() => {16 try {17 const result = onRejected(this.reason);18 resolvePromise(this, result, resolve, reject);19 } catch (error) {20 reject(error);21 }22 });23 }2425 if (this.state === 'pending') {26 this.onFulfilledCallbacks.push(value => {27 try {28 const result = onFulfilled(value);29 resolvePromise(this, result, resolve, reject);30 } catch (error) {31 reject(error);32 }33 });3435 this.onRejectedCallbacks.push(reason => {36 try {37 const result = onRejected(reason);38 resolvePromise(this, result, resolve, reject);39 } catch (error) {40 reject(error);41 }42 });43 }44 });45 };
5. Implementing Additional Methods
.catch()
1 MyPromise.prototype.catch = function(onRejected) {2 return this.then(null, onRejected);3 };
Promise.resolve()
1 MyPromise.resolve = function(value) {2 return new MyPromise(resolve => resolve(value));3 };
Promise.reject()
1 MyPromise.reject = function(reason) {2 return new MyPromise((_, reject) => reject(reason));3 };
Promise.all()
1 MyPromise.all = function(promises) {2 return new MyPromise((resolve, reject) => {3 let results = [];4 let completed = 0;56 promises.forEach((promise, index) => {7 promise.then(value => {8 results[index] = value;9 completed++;10 if (completed === promises.length) resolve(results);11 }, reject);12 });13 });14 };
Promise.race()
1 MyPromise.race = function(promises) {2 return new MyPromise((resolve, reject) => {3 promises.forEach(promise => {4 promise.then(resolve, reject);5 });6 });7 };
Conclusion
This implementation covers the fundamental concepts of Promises, including state management, asynchronous execution, chaining, and additional methods. With this foundation, you can now understand and debug Promise-based JavaScript more effectively.