Blog
4 min read

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 await inside a function marked async (or at the top level of a module).
  • An async function always returns a Promise, so whoever calls it must await it 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.
  • await waits for it inside an async function; async functions return Promises themselves.
  • Use try/catch for errors and check res.ok with fetch.
  • Use Promise.all for independent work; never await inside forEach.
  • A missing await is 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