[Medium] π Promise
What is a Promise?β
Promise is an ES6 feature mainly introduced to solve callback hell and make asynchronous code easier to read and maintain. A Promise represents the eventual completion (or failure) of an async operation and its resulting value.
A Promise has three states:
- pending: initial state
- fulfilled: operation completed successfully
- rejected: operation failed
Basic Usageβ
Create a Promiseβ
const myPromise = new Promise((resolve, reject) => {
// asynchronous operation
const success = true;
if (success) {
resolve('Success!'); // Promise becomes fulfilled
} else {
reject('Failed!'); // Promise becomes rejected
}
});
myPromise
.then((result) => {
console.log(result); // 'Success!'
})
.catch((error) => {
console.log(error); // 'Failed!'
});
Real-world Example: handling API requestsβ
// shared function for API requests
function fetchData(url) {
return fetch(url)
.then((response) => {
// check whether response is in the 200~299 range
if (!response.ok) {
throw new Error('Network response was not ok ' + response.statusText);
}
return response.json(); // convert response to JSON and return
})
.catch((error) => {
// handle network issues or request failures
console.log('There has been a problem with your fetch operation:', error);
throw error; // rethrow for upstream handling
});
}
fetchData('https://jsonplaceholder.typicode.com/users/1')
.then((userData) => {
console.log('User data received:', userData);
})
.catch((error) => {
console.log('Error:', error.message);
});
Promise Methodsβ
.then() / .catch() / .finally()β
promise
.then((result) => {
// handle success
return result;
})
.catch((error) => {
// handle error
console.error(error);
})
.finally(() => {
// runs regardless of success or failure
console.log('Promise completed');
});
Promise.all()β
Resolves when all Promises resolve, and rejects immediately when any Promise rejects.
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve) =>
setTimeout(() => resolve('foo'), 100)
);
const promise3 = Promise.resolve(42);
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values); // [3, 'foo', 42]
});
When to use: continue only after multiple API calls all succeed.
Promise.race()β
Returns the result of the first Promise that settles (fulfilled or rejected).
const promise1 = new Promise((resolve) =>
setTimeout(() => resolve('first'), 500)
);
const promise2 = new Promise((resolve) =>
setTimeout(() => resolve('second'), 100)
);
Promise.race([promise1, promise2]).then((value) => {
console.log(value); // 'second' (faster)
});
When to use: request timeout handling, or taking the fastest response.
Promise.allSettled()β
Waits for all Promises to settle (fulfilled/rejected), then returns all outcomes.
const promise1 = Promise.resolve(3);
const promise2 = Promise.reject('Error');
const promise3 = Promise.resolve(42);
Promise.allSettled([promise1, promise2, promise3]).then((results) => {
console.log(results);
// [
// { status: 'fulfilled', value: 3 },
// { status: 'rejected', reason: 'Error' },
// { status: 'fulfilled', value: 42 }
// ]
});
When to use: you need all outcomes, even if some fail.
Promise.any()β
Resolves with the first fulfilled Promise. Rejects only if all Promises reject.
const promise1 = Promise.reject('Error 1');
const promise2 = new Promise((resolve) =>
setTimeout(() => resolve('Success'), 100)
);
const promise3 = Promise.reject('Error 2');
Promise.any([promise1, promise2, promise3]).then((value) => {
console.log(value); // 'Success'
});
When to use: fallback resources where one success is enough.
Interview Questionsβ
Question 1: Promise chaining and error handlingβ
Predict the output:
Promise.resolve(1)
.then((x) => x + 1)
.then(() => {
throw new Error('My Error');
})
.catch((e) => 1)
.then((x) => x + 1)
.then((x) => console.log(x))
.catch((e) => console.log('This will not run'));
Walkthroughβ
Promise.resolve(1) // returns 1
.then((x) => x + 1) // x = 1, returns 2
.then(() => {
throw new Error('My Error'); // throws -> go to catch
})
.catch((e) => 1) // catches and returns normal value 1
.then((x) => x + 1) // x = 1, returns 2
.then((x) => console.log(x)) // prints 2
.catch((e) => console.log('This will not run')); // not executed
Answer: 2
Key Conceptsβ
catchcan recover with a normal value: whencatch()returns a normal value, the chain continues in fulfilled mode.thenaftercatchstill runs: because the error has been handled.- Final
catchdoes not run: no new error is thrown.
If you want the error to keep propagating, rethrow it in catch:
Promise.resolve(1)
.then((x) => x + 1)
.then(() => {
throw new Error('My Error');
})
.catch((e) => {
console.log('Error caught');
throw e; // rethrow
})
.then((x) => x + 1) // will not run
.then((x) => console.log(x)) // will not run
.catch((e) => console.log('This will run')); // will run
Question 2: Event Loop and execution orderβ
This question also tests Event Loop understanding.
Predict the output:
function a() {
console.log('Warlock');
}
function b() {
console.log('Druid');
Promise.resolve().then(() => {
console.log('Rogue');
});
}
function c() {
console.log('Mage');
}
function d() {
setTimeout(c, 100);
const temp = Promise.resolve().then(a);
console.log('Warrior');
setTimeout(b, 0);
}
d();
Understand order in d()β
function d() {
setTimeout(c, 100); // 4. macro task (100ms delay)
const temp = Promise.resolve().then(a); // 2. micro task (after sync code)
console.log('Warrior'); // 1. sync code
setTimeout(b, 0); // 3. macro task (0ms, still macro)
}
Execution order:
- Synchronous code:
console.log('Warrior')->Warrior - Micro task:
Promise.resolve().then(a)-> runa()->Warlock - Macro tasks:
setTimeout(b, 0)runs first- run
b()->Druid - inside
b,Promise.resolve().then(...)is a micro task ->Rogue
- Macro task:
setTimeout(c, 100)runs later ->Mage
Answerβ
Warrior
Warlock
Druid
Rogue
Mage
Key Conceptsβ
- Sync code > Micro tasks (
Promise) > Macro tasks (setTimeout) .then()callbacks are micro tasks: they run after current macro task, before next macro tasksetTimeout(..., 0)is still macro task and runs after micro tasks
Question 3: Promise constructor sync vs async behaviorβ
Predict the output:
function printing() {
console.log(1);
setTimeout(function () {
console.log(2);
}, 1000);
setTimeout(function () {
console.log(3);
}, 0);
new Promise((resolve, reject) => {
console.log(4);
resolve(5);
}).then((foo) => {
console.log(6);
});
console.log(7);
}
printing();
// output ?
Important detailβ
The key point: code inside the Promise constructor runs synchronously.
Only .then() / .catch() callbacks are asynchronous.
Execution analysis:
console.log(1); // 1. sync
setTimeout(() => console.log(2), 1000); // 5. macro task (1000ms)
setTimeout(() => console.log(3), 0); // 4. macro task (0ms)
new Promise((resolve, reject) => {
console.log(4); // 2. sync (inside constructor)
resolve(5);
}).then((foo) => {
console.log(6); // micro task
});
console.log(7); // 3. sync
Execution flow:
- Sync: 1 -> 4 -> 7
- Micro task: 6
- Macro tasks (by delay): 3 -> 2
Answerβ
1
4
7
6
3
2
Key Conceptsβ
- Promise constructor body is synchronous:
console.log(4)is not async. - Only
.then()and.catch()are asynchronous micro tasks. - Order: sync code -> micro tasks -> macro tasks.
Common Pitfallsβ
1. Forgetting to returnβ
If you forget return in a Promise chain, the next .then() receives undefined.
// β wrong
fetchUser()
.then((user) => {
fetchPosts(user.id); // forgot return
})
.then((posts) => {
console.log(posts); // undefined
});
// β
correct
fetchUser()
.then((user) => {
return fetchPosts(user.id);
})
.then((posts) => {
console.log(posts); // correct data
});
2. Forgetting to catch errorsβ
Unhandled Promise rejections can crash flow and create noisy runtime errors.
// β may cause unhandled rejection
fetchData()
.then((data) => {
return processData(data);
})
.then((result) => {
console.log(result);
});
// β
add catch
fetchData()
.then((data) => {
return processData(data);
})
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error('Error occurred:', error);
});
3. Overusing new Promise(...)β
Do not wrap a function that already returns a Promise.
// β unnecessary wrapping
function fetchData() {
return new Promise((resolve, reject) => {
fetch(url)
.then((response) => resolve(response))
.catch((error) => reject(error));
});
}
// β
return directly
function fetchData() {
return fetch(url);
}
4. Chaining multiple catches incorrectlyβ
Each catch() handles errors from earlier parts of the chain.
Promise.resolve()
.then(() => {
throw new Error('Error 1');
})
.catch((e) => {
console.log('Caught:', e.message); // Caught: Error 1
})
.then(() => {
throw new Error('Error 2');
})
.catch((e) => {
console.log('Caught:', e.message); // Caught: Error 2
});
Related Topicsβ
- async/await - cleaner Promise syntax sugar
- Event Loop - deeper async model understanding