Code of the Day
IntermediateThe runtime

Async and the event loop

Why JavaScript does one thing at a time and still feels concurrent.

JavaScript / TSIntermediate10 min read
Recommended first
By the end of this lesson you will be able to:
  • Explain the single-threaded model and the event loop
  • Read and write async / await over promises
  • Avoid the classic "my value is undefined" async timing bug

JavaScript runs your code on a single thread — one thing at a time. Yet apps fetch data, wait on timers, and stay responsive. The trick is the : slow operations are handed off, and your code is notified later when they finish, instead of blocking the thread while it waits. This is the concurrency idea from the fundamentals track, made concrete.

The event loop, briefly

When you start a slow task — a network request, a timer — JavaScript registers a callback and moves on without waiting. When the task completes, its callback is placed on a queue, and the event loop runs it once the current work finishes. Nothing blocks; tasks take turns. That's how a single thread juggles thousands of things without freezing.

Promises and async / await

A represents a value that isn't ready yet — it will eventually resolve with a value or reject with an error. async/await lets you write code that reads top-to-bottom while still being non-blocking:

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

await pauses this function until the promise settles, then resumes — without freezing the rest of the program. An async function always returns a promise, so its callers await it too.

The classic timing bug

Forget to await, and you use a value before it exists:

const user = loadUser(1);   // a Promise, not a user!
console.log(user.name);     // undefined — oops

const user = await loadUser(1); // ✓ now it's the resolved value
console.log(user.name);

Handling errors and running in parallel

Two essentials once you're comfortable:

  • Errors in async code are caught with try / catch, just like synchronous code — that's much of why async/await exists:

    try {
      const user = await loadUser(1);
    } catch (err) {
      console.error("failed to load user", err);
    }
  • Parallelism: awaiting in sequence is slow when tasks are independent. Start them together with Promise.all:

    const [a, b] = await Promise.all([loadUser(1), loadUser(2)]);

    This kicks both requests off at once and waits for both — concurrency buying you real time savings.

"Why is it undefined?" is, nine times out of ten, a missing await — you're looking at the promise instead of the value it will produce.

Where to go next

You've now seen how two languages handle values, logic, and time. Revisit the Systems & Structure fundamentals — concurrency, APIs, testing — and watch how much of this syntax is those ideas in disguise.

Finished reading? Mark it complete to track your progress.

On this page