async/await: Clean Async Code
async/await: Clean Async Code
async/await transformed how JavaScript developers write asynchronous code. Instead of Promise chains, you write code that looks and reads like synchronous code — while still being fully non-blocking.
The Basics
// An async function always returns a Promise
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user; // Automatically wrapped in Promise.resolve()
}
// await pauses execution until the Promise resolves
// But it only pauses the async function — the rest of the JS thread keeps running
// Using the function
fetchUser(1).then(user => console.log(user));
// Or with await in another async function
async function main() {
const user = await fetchUser(1);
console.log(user.name);
}
Error Handling with try/catch
async function getWeather(city) {
try {
const response = await fetch(`https://api.weather.com/current?city=${city}`);
if (!response.ok) {
throw new Error(`API returned ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`Failed to get weather for ${city}:`, error.message);
return null; // Return fallback instead of crashing
}
}
This is cleaner than .then().catch() chains, especially when you have multiple async operations that each might fail.
Sequential vs Parallel Execution
This is the most important performance concept with async/await:
// SEQUENTIAL — each waits for the previous to complete (~3 seconds total)
async function loadDashboard_slow() {
const user = await fetchUser(1); // 1 second
const orders = await fetchOrders(1); // 1 second
const analytics = await fetchAnalytics(1); // 1 second
return { user, orders, analytics };
}
// PARALLEL — all start at the same time (~1 second total)
async function loadDashboard_fast() {
const [user, orders, analytics] = await Promise.all([
fetchUser(1),
fetchOrders(1),
fetchAnalytics(1),
]);
return { user, orders, analytics };
}
Rule: If requests don't depend on each other, run them in parallel with Promise.all. Sequential is for when you need the result of one to make the next request.
Real-World Patterns
Fetching with Full Error Handling
async function fetchWithHandling(url) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timed out after 5 seconds');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
Retry Logic
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = attempt * 1000; // Exponential backoff: 1s, 2s, 3s
console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Async Iteration
// Process items one at a time (useful to avoid rate limits)
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
// Process items in batches of N (parallel within batch, sequential between)
async function processInBatches(items, batchSize = 5) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(processItem));
results.push(...batchResults);
}
return results;
}
Async in React
// React: fetch data on component mount
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadUser() {
try {
setLoading(true);
const data = await fetchUser(userId);
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadUser();
}, [userId]);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return <UserCard user={user} />;
}
Common Mistakes
Mistake 1: Missing await
// BUG: data is a Promise, not the actual data
async function getUser(id) {
const data = fetch(`/api/users/${id}`).then(r => r.json()); // forgot await
console.log(data); // Promise { <pending> }
}
// FIX:
async function getUser(id) {
const data = await fetch(`/api/users/${id}`).then(r => r.json());
console.log(data); // The actual user object
}
Mistake 2: Unnecessary sequential awaits
// SLOW: these could run in parallel
const a = await getA();
const b = await getB(); // b waits for a, even though it doesn't need to
// FAST:
const [a, b] = await Promise.all([getA(), getB()]);
Mistake 3: async forEach
const ids = [1, 2, 3, 4, 5];
// BUG: forEach doesn't wait for async callbacks
ids.forEach(async (id) => {
await processId(id); // These all run concurrently but forEach returns before they complete
});
// FIX option 1: for...of (sequential)
for (const id of ids) {
await processId(id);
}
// FIX option 2: Promise.all (parallel)
await Promise.all(ids.map(id => processId(id)));
async/await vs Promises — When to Use What
Use async/await for:
- Most asynchronous code — it's more readable
- Complex logic with multiple await points
- Easy error handling with try/catch
Use Promises directly for:
Promise.all(),Promise.race(),Promise.allSettled()- When you need fine-grained control over the chain
- Simple one-liners that don't need error handling
They work together seamlessly — await works on any Promise.
Next lesson: Fetch API & Working with REST APIs — building real data-driven applications.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises