Async Patterns
Senior interviews don't ask "what is a Promise." They ask you to implement retry logic, handle race conditions, or design a concurrent task queue. This is the level you need.
Promise Fundamentals That Actually Matter
A Promise is a state machine: pending â fulfilled or pending â rejected. Once settled, it's immutable.
const p = new Promise((resolve, reject) => {
resolve('first');
resolve('second'); // ignored â already settled
reject('error'); // also ignored
});Microtask Queue
Promise callbacks (.then, .catch, .finally) run as microtasks â they execute before the next macrotask (setTimeout, I/O), but after the current synchronous call stack completes.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2Async/Await Is Not Just Sugar
async/await transforms sequential-looking code into promise chains, but it introduces behaviors you need to reason about:
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json(); // returns a Promise â await is implicit for caller
}The Unhandled Rejection Trap
async function loadData() {
const user = fetchUser(1); // no await â starts the promise
const posts = fetchPosts(1); // starts concurrently
return { user: await user, posts: await posts };
}If fetchPosts rejects before fetchUser resolves, you get an unhandled rejection because the rejection happens while we're still awaiting fetchUser. Use Promise.all instead:
async function loadData() {
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts(1),
]);
return { user, posts };
}Concurrency Combinators
| Method | Settles when | Use case |
|---|---|---|
Promise.all | All fulfill (or any reject) | Parallel independent requests |
Promise.allSettled | All settle | Batch ops where partial failure is OK |
Promise.race | First settles | Timeouts, fastest mirror |
Promise.any | First fulfills | Fallback chains |
Timeout Pattern
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
}
const data = await withTimeout(fetch('/api/slow'), 5000);Concurrency Control
Promise.all fires everything at once. In production, you often need to limit concurrency:
async function mapWithConcurrency(items, fn, concurrency = 5) {
const results = [];
const executing = new Set();
for (const [i, item] of items.entries()) {
const p = fn(item).then((result) => {
executing.delete(p);
return result;
});
executing.add(p);
results[i] = p;
if (executing.size >= concurrency) {
await Promise.race(executing);
}
}
return Promise.all(results);
}Error Handling Strategy
In production codebases, async error handling is architectural, not ad-hoc:
async function safeAsync(fn) {
try {
const result = await fn();
return [result, null];
} catch (error) {
return [null, error];
}
}
const [user, error] = await safeAsync(() => fetchUser(1));
if (error) {
// handle gracefully
}Retry with Exponential Backoff
async function retry(fn, { attempts = 3, baseDelay = 1000 } = {}) {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (error) {
if (i === attempts - 1) throw error;
const delay = baseDelay * 2 ** i + Math.random() * 100;
await new Promise((r) => setTimeout(r, delay));
}
}
}Interview Signal
Interviewers at the senior level look for:
- Concurrency awareness â knowing when
Promise.allvs sequential matters, and the foot-guns of concurrent awaits - Error boundaries â not just try/catch, but structured error propagation strategies
- Production patterns â retry, timeout, cancellation (AbortController), and concurrency limiting
- Mental model â understanding the event loop, microtask queue, and why
asyncfunctions always return promises