JavaScript Promises Deep Dive — States, Chaining, and async/await Internals
A Promise in JavaScript is an object representing the eventual completion or failure of an asynchronous operation. Think of it like a receipt from a food truck — you get the receipt immediately (the Promise), but the food arrives later (the resolved value). The Durga Antivirus Pro dashboard uses promises to coordinate multiple threat data sources, and Doda Browser uses them to manage parallel file downloads.
What You’ll Learn
- The three Promise states and how transitions work
- Promise chaining — passing values through
.then() - Static methods —
Promise.all,Promise.allSettled,Promise.race,Promise.any - How
async/awaitworks under the hood - The microtask queue and why promises are faster than
setTimeout - Error handling patterns for promise chains
- Parallel API calls with
Promise.all
Why Promises Matter
Before promises, JavaScript used callbacks for everything asynchronous. Callbacks worked for simple cases but fell apart with complex workflows — leading to “callback hell.” Promises give you a flat, composable way to handle async operations. The async/await syntax builds on promises, making async code read like synchronous code.
Learning Path
stateDiagram-v2
[*] --> Pending
Pending --> Fulfilled: resolve(value)
Pending --> Rejected: reject(reason)
Fulfilled --> [*]
Rejected --> [*]
Promise States
A Promise is always in one of three states:
- Pending — Initial state. The operation hasn’t completed yet.
- Fulfilled — The operation completed successfully. The
.then()handlers run. - Rejected — The operation failed. The
.catch()handlers run.
Once a promise settles (fulfills or rejects), its state is immutable — it can never change again:
const promise = new Promise((resolve, reject) => {
resolve('First');
reject('Second'); // Ignored — already resolved
resolve('Third'); // Ignored — already resolved
});
promise.then(console.log); // Output: First
Anticipating confusion: A promise can’t go from fulfilled back to pending, or from rejected to fulfilled. Once settled, it stays settled forever. This makes promises predictable.
Promise Chaining
Every .then() returns a new Promise, making chaining possible:
fetch('/api/users/1')
.then(response => {
if (!response.ok) throw new Error('HTTP error');
return response.json();
})
.then(user => {
console.log(user.name);
return fetch(`/api/posts?userId=${user.id}`);
})
.then(response => response.json())
.then(posts => {
console.log(`User has ${posts.length} posts`);
})
.catch(error => {
console.error('Any error in the chain:', error);
});How the chain works:
- If
.then()returns a value, the next.then()receives it. - If
.then()returns a Promise, the next.then()waits for it. - If any
.then()throws or returns a rejected Promise, execution jumps to the nearest.catch().
Value Transformations
Promise.resolve(5)
.then(x => x * 2) // Returns 10
.then(x => Promise.resolve(x + 3)) // Returns Promise(13)
.then(console.log); // Output: 13
Error Recovery in Chains
You can recover from errors mid-chain:
fetch('/api/data')
.then(response => {
if (!response.ok) return { cached: true, data: getFromCache() };
return response.json();
})
.then(data => {
console.log('Using data:', data);
});Static Methods
Promise.all — Wait for All (Fail Fast)
const urls = ['/api/users', '/api/posts', '/api/comments'];
const promises = urls.map(url => fetch(url).then(r => r.json()));
try {
const [users, posts, comments] = await Promise.all(promises);
console.log(`Loaded ${users.length} users, ${posts.length} posts`);
} catch (error) {
console.error('One request failed:', error);
}Behavior: If any promise rejects, Promise.all immediately rejects with that error. Other promises continue running but their results are discarded.
Promise.allSettled — Wait for All (Never Rejects)
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/invalid').then(r => r.json()),
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`Request ${i} succeeded:`, result.value);
} else {
console.log(`Request ${i} failed:`, result.reason);
}
});Output:
Request 0 succeeded: [...]
Request 1 failed: TypeError: Failed to fetchPromise.race — First to Settle Wins
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
);
const fetchWithTimeout = Promise.race([
fetch('/api/data'),
timeout
]);Promise.any — First to Fulfill Wins
const servers = [
fetch('https://backup1.example.com/data'),
fetch('https://backup2.example.com/data'),
fetch('https://primary.example.com/data'),
];
try {
const fastest = await Promise.any(servers);
console.log('Got data from fastest server');
} catch (error) {
console.error('All servers failed:', error.errors);
}Comparison table:
| Method | Resolves | Rejects |
|---|---|---|
Promise.all | All fulfill | First rejection |
Promise.allSettled | All settle | Never |
Promise.race | First settle | First rejection |
Promise.any | First fulfillment | All reject |
Async/Await Internals
async/await is syntactic sugar over promises. The JavaScript engine transforms async functions into state machines — like generators that yield control:
// This async function:
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
// Is essentially this (simplified):
function loadUser(id) {
return fetch(`/api/users/${id}`)
.then(response => response.json());
}Key insight: Every await creates a “pause point” where the function yields control back to the event loop. While the promise is pending, other code can run:
async function example() {
console.log('A');
await Promise.resolve(); // Yields to microtask queue
console.log('B');
}
example();
console.log('C');
// Output: A, C, B
The Microtask Queue
Promise callbacks (.then(), .catch(), .finally() and code after await) go into the microtask queue, which has priority over the macrotask queue (setTimeout, setInterval, I/O):
console.log('1: Start');
setTimeout(() => console.log('2: Timeout'), 0);
Promise.resolve().then(() => console.log('3: Promise'));
console.log('4: End');
// Output:
// 1: Start
// 4: End
// 3: Promise ← microtask runs before macrotask
// 2: Timeout
Why this matters: Promises are always processed before setTimeout callbacks, even when the timeout is 0ms. This means promise-based code often runs faster than callback-based alternatives.
Error Handling in Promise Chains
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (response.status === 404) {
throw new NotFoundError('User not found');
}
if (!response.ok) {
throw new ApiError(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof NotFoundError) {
return null; // Graceful degradation
}
// Log and re-throw unexpected errors
console.error('Unexpected error:', error);
throw error;
}
}Best practice: Handle specific error types with instanceof checks. Let unexpected errors propagate to a global handler.
Common Mistakes
1. Not returning a promise from .then()
fetch('/api/data')
.then(response => {
response.json(); // Missing return!
})
.then(data => {
console.log(data); // undefined!
});Fix: Always return inside .then() if you want the next handler to receive the value.
2. Sequential promises that should be parallel
const a = await fetch('/api/a');
const b = await fetch('/api/b'); // Waits for 'a' to finish
Fix: Use Promise.all for independent requests.
3. Forgetting to handle promise rejections
function loadData() {
return fetch('/api/data').then(r => r.json());
// If fetch fails, promise rejects — unhandled!
}
loadData();Fix: Always add .catch() or handle with try/catch in the caller.
4. Creating promise chains inside loops
for (const url of urls) {
const data = await fetch(url); // Sequential!
}Fix: Create promises first, then use Promise.all.
5. Nesting promises instead of chaining
fetch('/api/a').then(dataA => {
fetch('/api/b').then(dataB => {
// Nesting recreates callback hell
});
});Fix: Return inner promises and chain flatly.
Practice Questions
What happens if you call
resolve()twice on the same promise? Only the first call has an effect. Once a promise settles, it’s immutable.What’s the difference between
Promise.allandPromise.allSettled?Promise.allrejects fast (if any promise rejects).Promise.allSettledwaits for all to complete and returns results for each, never rejecting.Why does Promise code run before setTimeout(0)? Promise
.then()callbacks go to the microtask queue, which is processed before the macrotask queue where setTimeout callbacks go.How does
async/awaitrelate to promises?async/awaitis syntactic sugar over promises. Anasyncfunction always returns a promise, andawaitunwraps the promise value.
Challenge: Implement a promisePool(tasks, concurrency) function that runs async tasks with a maximum concurrency limit, returning all results.
Mini Project: Parallel API Data Loader
async function loadAllData(userIds) {
const results = await Promise.allSettled(
userIds.map(id =>
fetch(`https://api.example.com/users/${id}`)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
)
);
const users = [];
const errors = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
users.push(result.value);
} else {
errors.push({ userId: userIds[index], error: result.reason });
}
});
return { users, errors };
}
const { users, errors } = await loadAllData([1, 2, 3]);
console.log(`Loaded ${users.length} users, ${errors.length} errors`);FAQ
What’s Next
| Lesson | Description |
|---|---|
| JavaScript Home | Back to the JavaScript hub |
| https://tutorials.dodatech.com/programming-languages/javascript/js-async/ | Async JavaScript fundamentals |
| https://tutorials.dodatech.com/programming-languages/javascript/js-error-handling/ | Error handling patterns |
| https://tutorials.dodatech.com/programming-languages/javascript/js-fetch-api/ | Fetch API — HTTP requests |
| Node.js | Async patterns in Node.js |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
What’s Next
Congratulations on completing this JavaScript Promises Deep Dive tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro