Async/Await in JavaScript, Explained Simply (With the Mistakes AI Code Makes)
Why some JavaScript code has to wait, what a Promise is, how async and await work, how to handle errors, how to run things in parallel, and the common async bugs — like the missing await — that show up in AI-generated code.
Look at almost any JavaScript in a modern app and you'll see async and await everywhere. They're how JavaScript deals with waiting — for a database, an API, a file. Understanding them explains a whole family of confusing bugs.
Why waiting is a problem
Some operations take time: fetching data from an API might take 300 milliseconds. JavaScript doesn't stop and wait — if it did, your whole page would freeze. Instead, it starts the operation and carries on with other work, coming back when the result is ready.
That's great for performance, but it means the result isn't available on the next line unless you explicitly wait for it.
Promises: an IOU for a value
When you start something slow, you immediately get back a Promise — an object that says "I'll have a value for you later". A Promise is:
- pending while the work is happening,
- fulfilled with a value when it succeeds,
- rejected with an error when it fails.
const promise = fetch("/api/books"); // returns immediately, still pending
console.log(promise); // Promise { <pending> } — not the books!
await: wait for the IOU
await pauses this function until the Promise settles, then gives you the value:
async function loadBooks() {
const res = await fetch("/api/books");
const books = await res.json();
console.log(books); // the actual data
}
Rules:
- You can only use
awaitinside a function markedasync(or at the top level of a module). - An
asyncfunction always returns a Promise, so whoever calls it mustawaitit too.
Handling errors
When an awaited Promise is rejected, await throws an error. Catch it with try/catch:
async function loadBooks() {
try {
const res = await fetch("/api/books");
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return await res.json();
} catch (err) {
console.error("Couldn't load books", err);
return [];
}
}
Note the res.ok check: fetch only rejects on network failures. A 404 or 500 response still "succeeds", so you have to check the status yourself. (HTTP status codes explained)
Running things in parallel
Awaiting one after another runs them in sequence:
const user = await getUser(); // 300ms
const orders = await getOrders(); // then another 300ms → 600ms total
If they don't depend on each other, start both and wait for both with Promise.all:
const [user, orders] = await Promise.all([getUser(), getOrders()]); // ~300ms
The bugs AI-generated code makes
1. The missing await
const user = getUser(); // forgot await
console.log(user.name); // undefined — user is a Promise
Symptoms: undefined where you expected data, [object Promise] on screen, or cannot read properties of undefined. Look for an async function called without await.
2. await inside forEach
items.forEach(async (item) => {
await save(item);
});
console.log("all saved"); // runs before anything is saved
forEach doesn't wait for async callbacks. Use a for...of loop (one at a time) or Promise.all(items.map(save)) (all at once).
3. Unhandled rejections
An async call without await or .catch that fails produces an "unhandled promise rejection" — and in Node.js, that can crash your server. Every Promise should be awaited or have a .catch.
4. Forgetting the server can be slow
Code that assumes data is instantly there — rendering books.map(...) before the request finished. Show a loading state until the data arrives.
.then() — the older style
You'll also see Promises handled with .then():
fetch("/api/books")
.then((res) => res.json())
.then((books) => console.log(books))
.catch((err) => console.error(err));
It does the same thing. async/await is usually easier to read, especially with several steps.
The summary
- Slow operations return a Promise: a value that arrives later.
awaitwaits for it inside anasyncfunction;asyncfunctions return Promises themselves.- Use
try/catchfor errors and checkres.okwithfetch. - Use
Promise.allfor independent work; neverawaitinsideforEach. - A missing
awaitis behind many "undefined" bugs.
EasySpawn gives Claude Code a server where it can actually run your code and see the real output — so async bugs show up as real errors it can fix, not guesses. See how it works or join the waitlist.
Related: Debugging for Beginners · How to Read an Error Message · What Is an API? · Browser Developer Tools for Beginners
Keep reading
"Cannot Read Properties of Undefined": What It Means and How to Fix It
The most common JavaScript error, explained for beginners: what "TypeError: Cannot read properties of undefined (reading 'map')" means, the five usual causes, how to find the exact line, and the right fixes — not just sprinkling ?. everywhere.
What Is TypeScript? JavaScript With Labels, Explained for Beginners
TypeScript is JavaScript plus types: labels that say what kind of value each thing holds, checked before your code runs. What it looks like, why AI tools default to it, what .ts and .tsx files are, and how to read the red squiggles.