async/await without the pitfalls
Async/await is the bread and butter of modern JavaScript. It makes asynchronous code look synchronous, which is great for readability. But it comes with its own set of footguns that can bite you in production. Here's how to avoid them.
Pitfall 1: Forgetting await in a loop
You might write something like this, expecting each request to finish before the next starts:
async function fetchAll(urls) {
const results = [];
for (const url of urls) {
const res = await fetch(url); // this is fine, but see below
results.push(await res.json());
}
return results;
}
That's actually correct. The issue arises when you forget await inside a .map() or .forEach():
// Wrong: map returns an array of promises, not data
const data = urls.map(async (url) => {
const res = await fetch(url);
return res.json();
});
// data is now an array of promises, not the JSON data
async functions always return a promise. So if you use map with an async callback, you get an array of promises. To fix it, use Promise.all:
const data = await Promise.all(urls.map(async (url) => {
const res = await fetch(url);
return res.json();
}));
But beware: Promise.all fails fast. If one request fails, the whole thing rejects. If you need to handle failures individually, use Promise.allSettled instead.
Pitfall 2: Swallowing errors silently
A common mistake is to catch an error and do nothing, which makes debugging a nightmare:
try {
const data = await fetchData();
// process data
} catch (error) {
// do nothing? bad!
}
Always at least log the error. Even better, handle it gracefully or rethrow it:
try {
const data = await fetchData();
} catch (error) {
console.error('Failed to fetch data:', error);
throw error; // rethrow if you want the caller to handle it
}
If you're using async/await, unhandled promise rejections can crash your app in Node.js. Always have a catch or a global handler.
Pitfall 3: Sequential execution when you need parallel
Using await inside a loop makes requests run one after another. If they're independent, that's a performance hit:
// Slow: sequential
for (const id of ids) {
const user = await getUser(id);
console.log(user);
}
// Fast: parallel
const users = await Promise.all(ids.map(getUser));
users.forEach(console.log);
But don't go overboard. Parallel requests can overwhelm a server or hit rate limits. A good middle ground is to batch them with Promise.all in chunks.
Pitfall 4: Using async when you don't need it
If a function doesn't have await inside, making it async is unnecessary and can cause subtle issues:
// Unnecessary async
async function double(x) {
return x * 2;
}
// This returns a promise, so you need to await it even though it's sync
It also changes error handling. If you throw inside an async function, it becomes a rejected promise, not a synchronous exception. Only use async when you actually await something.
Pitfall 5: Ignoring cancellation
Async/await doesn't have built-in cancellation. If you start a fetch and the user navigates away, you might still update the UI or leak resources. A simple pattern is to use an AbortController:
const controller = new AbortController();
try {
const res = await fetch(url, { signal: controller.signal });
// ...
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted');
} else {
throw error;
}
}
// To cancel:
controller.abort();
For more complex scenarios, libraries like p-cancelable exist, but sometimes a simple flag works:
let isCancelled = false;
async function longTask() {
while (!isCancelled) {
// do work
}
}
Pitfall 6: Forgetting that await blocks the event loop
await doesn't block the event loop, but it does pause the function. If you have a long-running synchronous operation inside an async function, it will block everything else. For CPU-heavy tasks, use setImmediate or worker threads to yield control.
Final tips
- Always use
try/catchor.catch()on every promise you create. - Prefer
Promise.allSettledwhen you need to handle partial failures. - Use
Promise.racefor timeouts, but be careful about unhandled rejections from the losing promise. - Lint your code with rules that catch missing
await(likerequire-awaitin ESLint).
Async/await is a powerful tool, but it's not magic. Understand these pitfalls, and you'll write more robust asynchronous code.
Happy coding!
Top comments (0)