Mastering Async/Await in JavaScript: A Practical Guide to Efficient Coding
Async/await isn’t magic — but it sure feels like it when you’re knee-deep in callback hell. JavaScript’s single-threaded nature means we rely heavily on asynchronous operations, and async/await gives us a clean, readable way to handle promises without losing our minds. If you're still nesting .then() chains or writing IIFEs around fetch, it’s time to level up.
Let’s cut through the noise and focus on how to use async/await effectively — with real code, common pitfalls, and patterns that scale.
1. Understand What Async/Await Actually Is
async/await is syntactic sugar over promises. That’s it. An async function always returns a promise, and await pauses execution until that promise resolves.
async function getData() {
const res = await fetch('/api/data');
const data = await res.json();
return data;
}
// This returns a promise
getData().then(data => console.log(data));
No callbacks. No .then() chains. Just linear-looking code that behaves asynchronously.
Key point: await only works inside async functions. Trying to use it at the top level (outside a module or function) will throw an error — unless you're in a modern environment with top-level await (ES modules in Node.js or browsers).
2. Handle Errors Like a Pro (Don’t Ignore Rejections)
One of the biggest mistakes? Forgetting that await can throw.
async function badExample() {
const res = await fetch('/api/data'); // What if the network fails?
const data = await res.json();
return data;
}
If the request fails, this function throws — and if you don’t catch it, your app crashes or silently fails.
Use try/catch:
async function getSafeData() {
try {
const res = await fetch('/api/data');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return data;
} catch (err) {
console.error('Fetch failed:', err);
return null; // or throw, depending on your needs
}
}
Or, if you want to propagate the error up:
async function getStrictData() {
const res = await fetch('/api/data');
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);
return res.json();
}
// Caller handles the error
getStrictData().catch(err => console.log('Handled:', err));
Pro tip: Wrap third-party API calls in try/catch. Assume everything can (and will) fail.
3. Don’t Block the Event Loop — Run in Parallel When You Can
await is sequential. If you await three independent API calls one after another, you’re adding up their latencies.
// SLOW: Each call waits for the previous
async function sequentialCalls() {
const user = await fetch('/api/user').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
const comments = await fetch('/api/comments').then(r => r.json());
return { user, posts, comments };
}
Instead, start all requests at once and await them together:
// FAST: All requests fire immediately
async function parallelCalls() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { user, posts, comments };
}
Promise.all() fails fast — if one promise rejects, the whole thing rejects. If you need results even when some fail, use Promise.allSettled():
const results = await Promise.allSettled([
fetch('/api/user').then(r => r.json()),
fetch('/api/broken').then(r => r.json()) // might fail
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});
4. Use Async/Await in Loops — But Carefully
Looping with await can bite you if you’re not paying attention.
This is bad (sequential, slow):
async function badLoop(urls) {
const results = [];
for (const url of urls) {
const res = await fetch(url); // Waits for each one
results.push(await res.json());
}
return results;
}
If you have 10 URLs, this takes ~10x the average request time.
If the requests are independent, fire them all at once:
async function goodLoop(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
return Promise.all(promises);
}
But if you need sequential execution (e.g., rate-limited API), then await in the loop is correct:
async function rateLimitedLoop(urls) {
const results = [];
for (const url of urls) {
const res = await fetch(url);
results.push(await res.json());
await new Promise(r => setTimeout(r, 200)); // 5 req/sec
}
return results;
}
Rule of thumb: Only `await
☕ Professional
Top comments (0)