Skip to main content

Promise Fundamentals

What is a Promise?

A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It exists in one of three states:

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: Operation completed successfully
  • Rejected: Operation failed

Creating a Promise

const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation
if (/* operation successful */) {
resolve(result);
} else {
reject(error);
}
});

Promise Methods

Instance Methods

  1. .then(): Handles successful Promise resolution
myPromise
.then((result) => {
// Handle successful result
})
.catch((error) => {
// Handle errors
});
  1. .catch(): Handles Promise rejection
myPromise.catch((error) => {
// Handle any errors
});
  1. .finally(): Executes regardless of Promise state
myPromise
.then((result) => {/* success */})
.catch((error) => {/* error */})
.finally(() => {
// Always runs, useful for cleanup
});

Static Methods

  1. Promise.resolve(): Creates a resolved Promise
const resolvedPromise = Promise.resolve(value);
  1. Promise.reject(): Creates a rejected Promise
const rejectedPromise = Promise.reject(error);
  1. Promise.all(): Waits for all Promises to resolve
Promise.all([promise1, promise2, promise3])
.then((results) => {
// All promises resolved
})
.catch((error) => {
// If any promise is rejected
});
  1. Promise.race(): Resolves/rejects with first settled Promise
Promise.race([promise1, promise2])
.then((firstResult) => {
// First promise to settle
});
  1. Promise.allSettled(): Waits for all Promises to settle
Promise.allSettled([promise1, promise2])
.then((results) => {
// Array of settlement results
});
  1. Promise.any(): Resolves with first fulfilled Promise
Promise.any([promise1, promise2])
.then((firstFulfilledResult) => {
// First promise fulfilled
})
.catch(AggregateError);

Advanced Promise Patterns

Chaining Promises

fetchUser()
.then(user => fetchUserPosts(user.id))
.then(posts => processPosts(posts))
.catch(handleError);

Async/Await Syntax

async function fetchData() {
try {
const user = await fetchUser();
const posts = await fetchUserPosts(user.id);
return processPosts(posts);
} catch (error) {
handleError(error);
}
}

Common Pitfalls

  1. Unhandled Rejections: Always add .catch() or use try/catch
  2. Returning New Promises: Each .then() can return a new Promise
  3. Error Propagation: Errors automatically propagate through Promise chain

Performance Considerations

  • Avoid nested Promises
  • Use Promise.all() for concurrent operations
  • Prefer async/await for readability

Best Practices

  • Always handle potential errors
  • Use meaningful error messages
  • Keep Promise chains flat and readable
  • Leverage async/await for complex asynchronous logic

Promise Polyfill

class MyPromise {
static STATUS = Object.freeze({
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
});

#status = MyPromise.STATUS.PENDING;
#value = null;
#successCbs = [];
#rejectedCbs = [];

constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('Executor must be a function');
}

try {
executor(this.#resolve, this.#reject);
} catch (e) {
this.#reject(e);
}
}

#resolve = (value) => {
if (this.#status !== MyPromise.STATUS.PENDING) return;

if (value === this) {
return this.#reject(new TypeError("A promise cannot be resolved with itself"));
}

if (value instanceof MyPromise) {
return value.then(this.#resolve, this.#reject);
}

if (value?.then && typeof value.then === 'function') {
return new MyPromise(value.then.bind(value)).then(this.#resolve, this.#reject);
}

this.#value = value;
this.#status = MyPromise.STATUS.FULFILLED;
this.#successCbs.forEach(cb => queueMicrotask(() => cb(value)));
};

#reject = (error) => {
if (this.#status !== MyPromise.STATUS.PENDING) return;

this.#value = error;
this.#status = MyPromise.STATUS.REJECTED;
this.#rejectedCbs.forEach(cb => queueMicrotask(() => cb(error)));
};

then = (onFulfilled, onRejected) => {
return new MyPromise((resolve, reject) => {
const handleQueueMicroTask = (isSuccess, _value) => {
queueMicrotask(() => {
try {
const value = isSuccess
? (typeof onFulfilled === 'function' ? onFulfilled(_value) : _value)
: (typeof onRejected === 'function' ? onRejected(_value) : reject(_value));

resolve(value);
} catch (e) {
reject(e);
}
});
};

switch (this.#status) {
case MyPromise.STATUS.PENDING:
this.#successCbs.push((val) => handleQueueMicroTask(true, val));
this.#rejectedCbs.push((err) => handleQueueMicroTask(false, err));
break;
case MyPromise.STATUS.FULFILLED:
handleQueueMicroTask(true, this.#value);
break;
case MyPromise.STATUS.REJECTED:
handleQueueMicroTask(false, this.#value);
break;
}
});
};

catch = (onRejected) => {
return this.then(null, onRejected);
};

finally = (onFinally) => {
return this.then(
(val) => MyPromise.resolve(onFinally()).then(() => val),
(error) => MyPromise.resolve(onFinally()).then(() => {
throw error;
})
);
};

static resolve = (value) => {
if (value instanceof MyPromise) return value;
if (value?.then && typeof value.then === 'function') {
return new MyPromise(value.then.bind(value));
}
return new MyPromise((res) => res(value));
};

static reject = (error) => {
return new MyPromise((_, rej) => rej(error));
};

static all = (promises) => {
return new MyPromise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}

const results = [];
let completedCount = 0;

if (promises.length === 0) {
return resolve(results);
}

promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
(value) => {
results[index] = value;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
},
(error) => {
reject(error);
}
);
});
});
};

static allSettled = (promises) => {
return new MyPromise((resolve) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}

const results = [];
let completedCount = 0;

if (promises.length === 0) {
return resolve(results);
}

promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
(value) => {
results[index] = { status: 'fulfilled', value };
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
},
(error) => {
results[index] = { status: 'rejected', reason: error };
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
}
);
});
});
};

static race = (promises) => {
return new MyPromise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}

promises.forEach((promise) => {
MyPromise.resolve(promise).then(resolve, reject);
});
});
};

static any = (promises) => {
return new MyPromise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}

const errors = [];
let rejectedCount = 0;

if (promises.length === 0) {
return reject(new AggregateError([], 'All promises were rejected'));
}

promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
(value) => {
resolve(value);
},
(error) => {
errors[index] = error;
rejectedCount++;
if (rejectedCount === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
}
);
});
});
};

static withResolvers = () => {
let resolve, reject;
const promise = new MyPromise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};

static try = (fn) => {
if (typeof fn !== 'function') {
throw new TypeError('Argument must be a function');
}
return new MyPromise((resolve, reject) => {
try {
resolve(fn());
} catch (e) {
reject(e);
}
});
};
}


// Example 1: MyPromise.all
const p1 = MyPromise.resolve(1);
const p2 = MyPromise.resolve(2);
const p3 = new MyPromise((resolve) => setTimeout(() => resolve(3), 1000));

MyPromise.all([p1, p2, p3]).then((results) => {
console.log(results); // [1, 2, 3] (after 1 second)
});

// Example 2: MyPromise.allSettled
const p4 = MyPromise.resolve(4);
const p5 = MyPromise.reject('Error 5');

MyPromise.allSettled([p4, p5]).then((results) => {
console.log(results);
// [
// { status: 'fulfilled', value: 4 },
// { status: 'rejected', reason: 'Error 5' }
// ]
});

// Example 3: MyPromise.race
const p6 = new MyPromise((resolve) => setTimeout(() => resolve('Fast'), 500));
const p7 = new MyPromise((resolve) => setTimeout(() => resolve('Slow'), 1000));

MyPromise.race([p6, p7]).then((result) => {
console.log(result); // 'Fast' (after 500ms)
});

// Example 4: MyPromise.any
const p8 = MyPromise.reject('Error 8');
const p9 = new MyPromise((resolve) => setTimeout(() => resolve('Success 9'), 1000));

MyPromise.any([p8, p9]).then((result) => {
console.log(result); // 'Success 9' (after 1 second)
});

MyPromise.any([MyPromise.reject('Error 10'), MyPromise.reject('Error 11')]).catch((error) => {
console.log(error.errors); // ['Error 10', 'Error 11']
});

// Example 5: withResolvers
const { promise, resolve, reject } = MyPromise.withResolvers();
setTimeout(() => resolve('Resolved with withResolvers!'), 1000);
promise.then(console.log); // 'Resolved with withResolvers!' after 1 second

// Example 6: try
MyPromise.try(() => {
console.log('Executing synchronous code');
if (Math.random() > 0.5) {
throw new Error('Random error in try');
}
return 'Success in try';
})
.then(console.log) // 'Success in try'
.catch(console.error); // 'Error: Random error in try'

Promise.all

Promise.myAll = function (promises = []) {
return new Promise((resolve, reject) => {
if (!promises.length) resolve([])
const result = []
let count = 0
promises.forEach((promise, index) => {
Promise.resolve(promise).then((value) => {
result[index] = value
count++
if (count === promises.length) {
resolve(result)
}
}).catch((error) => reject(error))
})
})
}

// Example usage:
const p1 = Promise.reject(3);
const p2 = 42;
const p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});

Promise.myAll([p1, p2, p3])
.then((values) => {
console.log(values); // [3, 42, "foo"]
})
.catch((error) => {
console.error(error);
});

Promise.allSettled

Promise.myAllSettled = function (promises = []) {
return Promise.all(
promises.map(promise => {
return Promise.resolve(promise).then((value) => ({
status: 'fulfilled', value
})).catch((reason => ({
status: 'rejected', reason
})))
})
)
}

// Example usage:
const p1 = Promise.resolve(3);
const p2 = Promise.reject('An error occurred');
const p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});

Promise.myAllSettled([p1, p2, p3])
.then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 3 },
// { status: 'rejected', reason: 'An error occurred' },
// { status: 'fulfilled', value: 'foo' }
// ]
});

Promise Any

Promise.myAny = function (promises = []) {
return new Promise((resolve, reject) => {
let errors = [];
let rejectedCount = 0;
const totalPromises = promises.length;

if (!totalPromises) {
return reject(new AggregateError(errors, "All promises were rejected"));
}

promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(value => {
resolve(value);
})
.catch(error => {
errors[index] = error;
rejectedCount++;

if (rejectedCount === totalPromises) {
reject(new AggregateError(errors, "All promises were rejected"));
}
});
});
});
};

// Example usage:
const p1 = Promise.reject('Error 1');
const p2 = new Promise((resolve) => setTimeout(resolve, 100, 'Success 2'));
const p3 = new Promise((resolve) => setTimeout(resolve, 200, 'Success 3'));

Promise.myAny([p1, p2, p3])
.then(value => {
console.log('Resolved with value:', value); // "Resolved with value: Success 2"
})
.catch(error => {
console.error('Rejected with error:', error);
});

Promise Race

Promise.myRace = function (promises = []) {
return new Promise((resolve, reject) => {
promises.forEach(promise => {
Promise.resolve(promise)
.then(resolve)
.catch(reject);
});
});
};

// Example usage:
const p1 = new Promise((resolve) => setTimeout(resolve, 500, 'Success 1'));
const p2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'Error 2'));
const p3 = new Promise((resolve) => setTimeout(resolve, 200, 'Success 3'));

Promise.myRace([p1, p2, p3])
.then(value => {
console.log('Resolved with value:', value); // "Resolved with value: Success 2"
})
.catch(error => {
console.error('Rejected with error:', error); // "Rejected with error: Error 2"
});

Fetch with Retry

const fetchWithRetry = async (fetcher, maxRetry = 3, currentRetry = 0) => {
try {
const response = await fetcher()
if (response.ok) {
return await response.json()
}
}
catch (err) {
console.log('err', err);
if (maxRetry > 0) {
return fetchWithRetry(fetcher, maxRetry - 1, currentRetry + 1)
}
}
throw new Error(`Failed to fetch after ${currentRetry} retries`)
}


//Usage
const url = 'https://api.example.com'

const fetchData = async () => fetchWithRetry(() => fetch(url), 3)


fetchData()
.then(res => res.json())
.then(data => console.log(data))
.catch(error => console.log('failed to fetch', error))

Sleep

const sleep = (wait) => new Promise(res => setTimeout(res, wait))

async function fix() {
console.log('Start');
await sleep(2000); // Sleep for 2 seconds
console.log('End');
}

fix();

Retry Callback

function retry(fn, retries = 3, delay = 1000) {
const attempt = (triesLeft, resolve, reject, args) => {
fn(...args)
.then(resolve)
.catch(error => {
if (triesLeft === 0) {
reject(`Failed after ${retries} attempts: ${error}`);
} else {
console.log(`Retrying... Attempts left: ${triesLeft}`);
setTimeout(() => attempt(triesLeft - 1, resolve, reject, args), delay);
}
});
};

return (...args) => new Promise((resolve, reject) => attempt(retries, resolve, reject, args));
}

Example Usage

function unreliableFunction() {
return new Promise((resolve, reject) => {
const success = Math.random() > 0.7; // 30% chance of success
if (success) {
resolve("Success!");
} else {
reject("Random failure");
}
});
}

const retryUnreliable = retry(unreliableFunction, 3, 1000);

retryUnreliable()
.then(result => console.log(result))
.catch(error => console.error(error));

/*
Retrying... Attempts left: 3
Retrying... Attempts left: 2
Retrying... Attempts left: 1
Failed after 3 attempts: Random failure
*/