๐ Day 22 — Promises & Async/Await Deep Dive
Hey Guys, Asynchronous JavaScript is essential for network calls, timers, and background work. Today we’ll go deep — Promises internals, chaining patterns, async/await, concurrency, error handling, and useful utility wrappers.
๐ Recap: What is a Promise?
A Promise represents an eventual value: a pending computation that either fulfills with a value or rejects with an error. It’s a replacement for callback hell and enables clearer flow control.
⚙ Creating and using Promises
const p = new Promise((resolve, reject) => {
// async work
setTimeout(() => resolve('done'), 1000);
});
p.then(value => console.log(value)).catch(err => console.error(err));
๐ Chaining & returning promises
Returning a promise inside .then() chains it. This makes sequential async flows readable.
fetch('/users')
.then(res => res.json())
.then(users => fetch(`/posts?user=${users[0].id}`))
.then(res => res.json())
.then(posts => console.log(posts))
.catch(err => console.error('Chain error:', err));
⚡ async / await — syntactic sugar
async functions return a promise. Use await to pause until a promise resolves — which leads to code that reads top-to-bottom.
async function loadUserPosts() {
try {
const r1 = await fetch('/users');
const users = await r1.json();
const r2 = await fetch(`/posts?user=${users[0].id}`);
const posts = await r2.json();
console.log(posts);
} catch (err) {
console.error('Async error:', err);
}
}
loadUserPosts();
๐ง Parallel vs Sequential
Use Promise.all for parallel requests. Avoid awaiting sequentially when calls are independent.
// sequential (slower)
await fetch('/a');
await fetch('/b');
// parallel (faster)
const [aRes, bRes] = await Promise.all([fetch('/a'), fetch('/b')]);
const [a, b] = await Promise.all([aRes.json(), bRes.json()]);
๐ Useful patterns
1) Safe wrapper (to handle errors without try/catch everywhere)
const to = p => p.then(data => [null, data]).catch(err => [err, null]);
const [err, data] = await to(fetch('/api').then(r => r.json()));
if (err) {
console.error(err);
} else {
console.log(data);
}
2) Retry with backoff
async function retry(fn, attempts = 3, delay = 500) {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (e) {
if (i === attempts - 1) throw e;
await new Promise(r => setTimeout(r, delay * Math.pow(2, i)));
}
}
}
3) Timeout a promise
function withTimeout(p, ms) {
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms));
return Promise.race([p, timeout]);
}
await withTimeout(fetch('/slow'), 3000);
๐งช Error handling strategies
- Handle errors close to where recovery is possible; otherwise bubble up.
- Use centralized logging for production and friendly UI messages for users.
- For long chains, use the safe wrapper (
to()) or a top-level try/catch in async functions.
๐ง Advanced concurrency controls
When you need to limit concurrent requests (e.g., rate limits), implement a simple queue / pool.
// simple concurrency limiter (pool size = 3)
async function pool(tasks, size = 3) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p);
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= size) await Promise.race(executing);
}
return Promise.all(results);
}
๐ Practice tasks
- Refactor a chain of fetch calls into async/await and add a retry wrapper.
- Implement a safeFetch wrapper that returns [err,data].
- Build a small uploader that uploads 5 files but only 2 at a time (use pool).
๐ฏ Summary
Promises and async/await are essential for modern JavaScript. Learn to compose promises, handle errors, and control concurrency — these patterns turn brittle code into reliable systems.
Comments
Post a Comment