Async Patterns & Concurrency Control
Senior interviews don't ask "what is a Promise." They test whether you can implement retry with backoff, cancel in-flight requests, control concurrency, and handle race conditions. This is production-grade async engineering.
Promise Internals: The State Machine
ââââ resolve(value) ââââ Fulfilled (value)
Pending ââĪ
ââââ reject(reason) âââ Rejected (reason)Once settled, a Promise never changes state. This immutability is what makes Promises composable.
const p = new Promise((resolve, reject) => {
resolve('first');
resolve('second'); // Ignored â already fulfilled
reject('error'); // Ignored â already fulfilled
});Concurrency Combinators
| Combinator | Resolves when | Rejects when | Use case |
|---|---|---|---|
Promise.all | All fulfill | Any rejects | Parallel independent fetches |
Promise.allSettled | All settle (either way) | Never rejects | Batch ops with partial failure |
Promise.race | First settles | First settles (if rejection) | Timeouts, fastest response |
Promise.any | First fulfills | All reject (AggregateError) | Fallback chains, redundancy |
Promise.allSettled â The Production Choice
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(3), // This might fail
]);
const succeeded = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const failed = results.filter(r => r.status === 'rejected').map(r => r.reason);
// Process what succeeded, report what failed â don't throw away partial resultsTimeout Pattern
function withTimeout(promise, ms, message = 'Operation timed out') {
let timeoutId;
const timeout = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(message)), ms);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timeoutId));
}
const data = await withTimeout(fetch('/api/slow'), 5000);AbortController â Proper Cancellation
AbortController is the standard cancellation mechanism for fetch and other async operations:
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request cancelled');
} else {
throw err;
}
});
controller.abort(); // Cancels the requestReact Pattern: Cancel on Unmount/Re-render
function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, [url]);
return data;
}Timeout + Cancellation Combined
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
return response;
} finally {
clearTimeout(timeoutId);
}
}Retry with Exponential Backoff
async function retry(fn, options = {}) {
const { attempts = 3, baseDelay = 1000, maxDelay = 30000, shouldRetry = () => true } = options;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await fn(attempt);
} catch (error) {
const isLastAttempt = attempt === attempts - 1;
if (isLastAttempt || !shouldRetry(error, attempt)) throw error;
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
const jitter = delay * 0.1 * Math.random();
await new Promise(r => setTimeout(r, delay + jitter));
}
}
}
// Usage
const data = await retry(
() => fetch('/api/flaky').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}),
{
attempts: 3,
baseDelay: 1000,
shouldRetry: (err) => !err.message.includes('401'),
}
);Jitter is critical â without it, all clients retry at the same time after an outage, creating a "thundering herd."
Concurrency Limiting
Promise.all fires everything at once. When you have 1000 items and a rate-limited API, you need controlled concurrency:
async function mapConcurrent(items, fn, concurrency = 5) {
const results = new Array(items.length);
const executing = new Set();
for (let i = 0; i < items.length; i++) {
const promise = fn(items[i], i).then(result => {
results[i] = result;
executing.delete(promise);
});
executing.add(promise);
if (executing.size >= concurrency) {
await Promise.race(executing);
}
}
await Promise.all(executing);
return results;
}
// Process 1000 images, 5 at a time
const thumbnails = await mapConcurrent(
images,
(img) => generateThumbnail(img),
5
);Sequential vs Parallel Execution
// â Sequential (2x slower) â each awaits before starting next
const user = await fetchUser(id);
const posts = await fetchPosts(id);
// â
Parallel â both start immediately
const [user, posts] = await Promise.all([
fetchUser(id),
fetchPosts(id),
]);
// â
Parallel with independent error handling
const [userResult, postsResult] = await Promise.allSettled([
fetchUser(id),
fetchPosts(id),
]);The Waterfall Anti-Pattern
// â Serial waterfall â each query waits for the previous
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();
// â
Parallel when independent
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
// â
Waterfall when dependent (this is correct)
const user = await fetchUser(id);
const posts = await fetchPostsByAuthor(user.authorId);Error Handling Patterns
The Result Pattern (Rust/Go-style)
async function safeAsync(fn) {
try {
return [await fn(), null];
} catch (error) {
return [null, error];
}
}
const [user, error] = await safeAsync(() => fetchUser(1));
if (error) {
logger.error('Failed to fetch user', error);
return fallbackUser;
}Structured Error Types
class AppError extends Error {
constructor(message, code, cause) {
super(message);
this.code = code;
this.cause = cause;
}
}
class NetworkError extends AppError {
constructor(message, statusCode, cause) {
super(message, 'NETWORK_ERROR', cause);
this.statusCode = statusCode;
}
get isRetryable() {
return this.statusCode >= 500 || this.statusCode === 429;
}
}
async function fetchJSON(url) {
try {
const res = await fetch(url);
if (!res.ok) throw new NetworkError(`HTTP ${res.status}`, res.status);
return res.json();
} catch (err) {
if (err instanceof NetworkError) throw err;
throw new NetworkError('Network failure', 0, err);
}
}Async Generators & Iteration
For streaming or paginated data:
async function* paginate(fetchPage) {
let page = 1;
let hasMore = true;
while (hasMore) {
const result = await fetchPage(page);
yield* result.items;
hasMore = result.hasMore;
page++;
}
}
// Usage
for await (const user of paginate((p) => fetchUsers({ page: p }))) {
processUser(user);
}Debounced Async (Search Pattern)
function createDebouncedFetcher(fetchFn, delay = 300) {
let timeoutId = null;
let controller = null;
return async function(query) {
controller?.abort();
clearTimeout(timeoutId);
return new Promise((resolve, reject) => {
timeoutId = setTimeout(async () => {
controller = new AbortController();
try {
const result = await fetchFn(query, controller.signal);
resolve(result);
} catch (err) {
if (err.name !== 'AbortError') reject(err);
}
}, delay);
});
};
}Interview Signal
Senior candidates demonstrate:
- Combinator mastery â Choosing the right Promise combinator for the scenario
- Production patterns â Retry, timeout, cancellation, concurrency limiting
- Race condition awareness â AbortController for stale requests, latest-only patterns
- Error architecture â Structured errors, retryable vs terminal failures
- Performance thinking â Parallel vs sequential, avoiding waterfalls, controlled concurrency