Understanding async/await in JavaScript
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.