JavaScript & TypeScript Mastery
Async Patterns & Error Handling
4 min read
Asynchronous programming is where many candidates stumble. Interviewers test both your knowledge of Promise APIs and your ability to handle edge cases gracefully.
Promise Combinators
Know all four and when to use each:
const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 100));
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 500));
const fail = new Promise((_, reject) => setTimeout(() => reject('error'), 200));
// Promise.all — resolves when ALL resolve, rejects on FIRST rejection
await Promise.all([fast, slow]); // ['fast', 'slow']
await Promise.all([fast, fail, slow]); // throws 'error'
// Promise.allSettled — waits for ALL to settle (never rejects)
await Promise.allSettled([fast, fail, slow]);
// [
// { status: 'fulfilled', value: 'fast' },
// { status: 'rejected', reason: 'error' },
// { status: 'fulfilled', value: 'slow' }
// ]
// Promise.race — resolves/rejects with the FIRST to settle
await Promise.race([fast, slow]); // 'fast'
// Promise.any — resolves with the FIRST to fulfill (ignores rejections)
await Promise.any([fail, fast, slow]); // 'fast'
await Promise.any([fail]); // throws AggregateError
| Combinator | Resolves when | Rejects when |
|---|---|---|
all |
All fulfill | First rejection |
allSettled |
All settle | Never rejects |
race |
First settles | First rejection (if first to settle) |
any |
First fulfills | All reject (AggregateError) |
Implementing Promise.all
A common interview coding question:
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
return;
}
const results = new Array(promises.length);
let resolvedCount = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
results[index] = value;
resolvedCount++;
if (resolvedCount === promises.length) {
resolve(results);
}
},
(error) => {
reject(error);
}
);
});
});
}
Key details interviewers check:
- Results must maintain order (use
index, not push) - Handle non-Promise values via
Promise.resolve(promise) - Handle empty array edge case
- Only call
resolveonce (when all are done) - Reject immediately on first error
AbortController
Used to cancel fetch requests and other async operations:
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw error;
}
}
// AbortController can also cancel multiple operations
const controller = new AbortController();
// Pass the same signal to multiple fetch calls
const [users, posts] = await Promise.all([
fetch('/api/users', { signal: controller.signal }),
fetch('/api/posts', { signal: controller.signal }),
]);
// Cancel both if component unmounts (React pattern)
controller.abort();
Retry with Exponential Backoff
A practical pattern that combines async concepts:
async function retry(fn, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, attempt);
const jitter = delay * (0.5 + Math.random() * 0.5);
await new Promise(resolve => setTimeout(resolve, jitter));
}
}
}
// Usage
const data = await retry(
() => fetch('/api/data').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}),
3, // max 3 retries
1000 // start with 1s delay
);
Common Async Pitfalls
Pitfall 1: forEach does not await
// WRONG — fires all requests simultaneously, does not wait
const ids = [1, 2, 3];
ids.forEach(async (id) => {
await fetch(`/api/items/${id}`);
});
console.log('Done'); // Runs BEFORE fetches complete
// CORRECT — sequential
for (const id of ids) {
await fetch(`/api/items/${id}`);
}
console.log('Done'); // Runs after all fetches
// CORRECT — parallel
await Promise.all(ids.map(id => fetch(`/api/items/${id}`)));
console.log('Done'); // Runs after all fetches
Pitfall 2: Unhandled Promise rejections
// WRONG — no catch, unhandled rejection
async function loadData() {
const data = await fetch('/api/data');
return data.json();
}
loadData(); // If this rejects, nothing catches it
// CORRECT — always handle rejections
loadData().catch(console.error);
// Or in React, use error boundaries + useEffect cleanup
useEffect(() => {
let cancelled = false;
loadData().then(data => {
if (!cancelled) setData(data);
}).catch(err => {
if (!cancelled) setError(err);
});
return () => { cancelled = true; };
}, []);
Pitfall 3: Race conditions in React
// WRONG — stale data if requests complete out of order
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(setResults);
}, [query]);
// If query changes rapidly: "ab" request might resolve
// after "abc" request, showing wrong results
}
// CORRECT — cancel stale requests
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort();
}, [query]);
}
Interview tip: When discussing async code, always mention error handling, cancellation, and race conditions. This shows production-level thinking, not just textbook knowledge.
Now it's time to put these concepts into practice with the JavaScript Engine Simulator lab. :::