← all posts

Understanding async/await in JavaScript

2025-09-14 · ~7 min read

async/await in JavaScript looks easy until you actually use it under pressure. Here are a few things that confused me for embarrassingly long.

The basic shape

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

The async keyword means: this function returns a Promise. The await keyword means: pause here until the Promise resolves. So far so good.

Mistake 1: forgetting that async functions always return a Promise

const user = fetchUser(1);
console.log(user.name);  // undefined — user is a Promise

You either await it (inside another async function) or use .then():

const user = await fetchUser(1);              // works inside async
fetchUser(1).then(u => console.log(u.name));  // works anywhere

Mistake 2: awaiting in a loop when you didn't need to

const userIds = [1, 2, 3, 4, 5];
const users = [];
for (const id of userIds) {
  users.push(await fetchUser(id));  // sequential!
}

This makes 5 sequential requests. If they're independent, you want them in parallel:

const users = await Promise.all(userIds.map(fetchUser));

I once let a sequential loop ship to production and the page took 8 seconds to load. Promise.all dropped it to 800ms. The mistake is invisible in code review unless someone is paying attention.

Mistake 3: silent unhandled rejections

async function bad() {
  const res = await fetch('/api/thing');  // network fails
  return res.json();
}

bad();  // unhandled rejection — silent in some environments

You need either:

try {
  const data = await bad();
} catch (err) {
  console.error(err);
}

or:

bad().catch(err => console.error(err));

Modern Node.js will crash the process on unhandled rejection by default, which is honestly the right behavior. Older versions silently swallowed them.

Mistake 4: mixing async and synchronous I/O

async function readConfig() {
  const text = fs.readFileSync('config.json');  // blocks the event loop
  return JSON.parse(text);
}

The wrong way works. But you've defeated the point of async — your function looks like it's non-blocking and isn't. Use the async version:

const text = await fs.promises.readFile('config.json', 'utf-8');

The pattern I follow now

Any function that does I/O is async. Any function calling an async function is itself async. The whole call stack ends up async. It looks redundant but it keeps the model consistent — and once you're consistent, none of these mistakes are easy to make.