DEV Community

Stack Horizon
Stack Horizon

Posted on

async/await without the pitfalls

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
}));
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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/catch or .catch() on every promise you create.
  • Prefer Promise.allSettled when you need to handle partial failures.
  • Use Promise.race for timeouts, but be careful about unhandled rejections from the losing promise.
  • Lint your code with rules that catch missing await (like require-await in 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)