Difficulty: Intermediate
Explain async/await in JavaScript. What are the common patterns and pitfalls?
async/await is syntactic sugar over Promises that makes asynchronous code look synchronous. An async function always returns a Promise. The await keyword pauses execution until the promise settles, then returns its resolved value or throws the rejection.
Common patterns include sequential execution (await one after another), parallel execution (Promise.all with await), error handling (try/catch blocks), and retry logic. A major pitfall is accidentally running async operations sequentially when they could run in parallel.
Key rules: await can only be used inside async functions (or at the top level of ES modules). Error handling with try/catch replaces .then/.catch chains. If you forget to await a promise, you get the promise object instead of the value.
function delay(ms, value) {
return new Promise(res => setTimeout(() => res(value), ms));
}
async function sequential() {
const start = Date.now();
const a = await delay(100, 'A');
const b = await delay(100, 'B');
console.log(`Sequential: ${a}, ${b} in ${Date.now() - start}ms`);
}
async function parallel() {
const start = Date.now();
const [a, b] = await Promise.all([
delay(100, 'A'),
delay(100, 'B')
]);
console.log(`Parallel: ${a}, ${b} in ${Date.now() - start}ms`);
}
sequential();
parallel();
Sequential awaits finish one after another (200ms total). Promise.all runs them in parallel (100ms total). Always use parallel when operations are independent.
async function fetchData(shouldFail) {
if (shouldFail) throw new Error('Network error');
return { data: 'success' };
}
// Pattern 1: try/catch
async function withTryCatch() {
try {
const result = await fetchData(true);
console.log(result);
} catch (err) {
console.log('Caught:', err.message);
} finally {
console.log('Cleanup done');
}
}
// Pattern 2: .catch() on await
async function withCatch() {
const result = await fetchData(true).catch(err => {
console.log('Handled:', err.message);
return { data: 'fallback' };
});
console.log(result);
}
// Pattern 3: Go-style error tuple
async function safeAsync(promise) {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
}
await withTryCatch();
await withCatch();
const [err, data] = await safeAsync(fetchData(false));
console.log('Go-style:', err, data);
Three error handling patterns: classic try/catch, .catch() for inline fallbacks, and Go-style error tuples for clean handling without try/catch blocks.
// Process items sequentially with for...of
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processOne(item);
results.push(result);
}
return results;
}
function processOne(item) {
return new Promise(res =>
setTimeout(() => res(item.toUpperCase()), 50)
);
}
// Retry pattern
async function retry(fn, retries = 3, delay = 100) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (err) {
console.log(`Attempt ${i + 1} failed: ${err.message}`);
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, delay));
}
}
}
const results = await processItems(['hello', 'world']);
console.log(results);
let attempt = 0;
await retry(async () => {
attempt++;
if (attempt < 3) throw new Error('not yet');
return 'success';
}).then(r => console.log('Retry result:', r));
for...of with await processes items sequentially. The retry pattern is essential for network requests. Note: forEach does NOT await - use for...of instead.
async, await, Error Handling, Parallel Execution, Sequential Execution, Top-level await