Async and the event loop
Why JavaScript does one thing at a time and still feels concurrent.
- 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 event loop: 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 promise 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 whyasync/awaitexists: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.