Full-Stack Development

Why await Doesn't Make JavaScript Run in Order

Learn why await only pauses one async function—not the whole program—with a two-function example, log timeline, and when Promise.all() is better.

JS
Jay Simons
Author
Why await Doesn't Make JavaScript Run in Order

A lot of developers learn async/await and walk away with one idea:

I used await, so everything waits.

That sentence is almost true — and the "almost" is where the bugs live.

await pauses one async function. It does not pause the rest of your program. Timers still fire. Event handlers still run. Other async functions keep moving. JavaScript is still taking turns.

Once you see that, a lot of "why did this log first?" moments stop being mysterious.

The assumption

Here's the mental model that gets people in trouble:

JS
await doSomething();
// nothing else in the entire program can happen until this finishes

That would be convenient. It is also not how JavaScript works.

await is local. It only tells this function to pause at this line and come back later. Everything else in the program is free to keep running.

A short surprising example

Two functions. One setTimeout. A few console.log calls.

JS
function later(label, ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(label);
      resolve();
    }, ms);
  });
}

async function loadUser() {
  console.log('loadUser: start');
  await later('loadUser: done', 1000);
  console.log('loadUser: after await');
}

async function loadSettings() {
  console.log('loadSettings: start');
  await later('loadSettings: done', 200);
  console.log('loadSettings: after await');
}

console.log('program: start');
loadUser();
loadSettings();
console.log('program: after both calls');

If await froze the whole program, you would expect something like this:

  1. loadUser starts
  2. the program waits a full second
  3. loadUser finishes
  4. loadSettings starts
  5. then the rest of the logs appear

That is not what happens.

What actually logs, and when

Here is the timeline:

TEXT
0ms     program: start
0ms     loadUser: start
0ms     loadSettings: start
0ms     program: after both calls
200ms   loadSettings: done
200ms   loadSettings: after await
1000ms  loadUser: done
1000ms  loadUser: after await

loadUser() hits await and steps aside. loadSettings() starts immediately. The next line in the main script runs immediately too. A fifth of a second later, the faster timer wins — even though we called the slower function first.

The order of the function calls is not the order of the finish times.

What await actually pauses

An async function always returns a Promise. When that function hits await, three things happen:

  1. The current function pauses at that line.
  2. JavaScript records "come back here when this Promise settles."
  3. Control returns to whoever called the function.

That last part is the one people miss.

JS
async function loadUser() {
  await later('loadUser: done', 1000);
  // this line waits
}

loadUser();
// this line does not wait

loadUser() is paused. The caller is not.

If you want the caller to wait, the caller has to await too:

JS
await loadUser();
await loadSettings();

Now loadSettings() will not start until loadUser() finishes. That is sequential on purpose — because you chained the waits, not because await stopped the whole program.

How other code keeps running

While loadUser() is paused, JavaScript is still doing work.

In the example above, that work is a setTimeout. In a real app, it is usually one of these:

  • another async function you already started
  • a click handler
  • a fetch() that finishes first
  • a React/Svelte effect
  • the next line after you forgot to await

You do not need a deep event-loop lecture to use this well. One picture is enough:

TEXT
Call loadUser()
  → hits await
  → loadUser is parked
  → JavaScript looks for other work
  → loadSettings() runs
  → timers and network calls keep ticking
  → whichever Promise finishes first gets to resume

await is a bookmark, not a freeze frame.

When sequential awaits are right

Sometimes you do want one thing to finish before the next thing starts. That is what sequential awaits are for.

Use them when the second call needs the first result:

JS
async function loadDashboard(userId) {
  const user = await fetchUser(userId);
  const projects = await fetchProjects(user.teamId);

  return { user, projects };
}

You cannot fetch the projects until you know the team id. Waiting in order is the correct design.

The same rule applies to writes that must happen in sequence:

JS
await createCustomer(form);
await sendWelcomeEmail(form.email);

If the customer row is not there yet, the email step should not run.

When Promise.all() is better

If the tasks do not depend on each other, waiting in a line wastes time.

This version looks tidy and is often slower than it needs to be:

JS
async function loadPage() {
  const user = await fetchUser();
  const settings = await fetchSettings();
  const notifications = await fetchNotifications();

  return { user, settings, notifications };
}

Each request sits around until the previous one finishes. If each call takes 300ms, you pay about 900ms.

These three requests do not need each other. Start them together:

JS
async function loadPage() {
  const [user, settings, notifications] = await Promise.all([
    fetchUser(),
    fetchSettings(),
    fetchNotifications()
  ]);

  return { user, settings, notifications };
}

Now the slowest request sets the wait time, not the sum of all three. If they all take 300ms, you wait about 300ms.

That is the same idea as the first example. loadUser() and loadSettings() overlapped because we started both without waiting. Promise.all() makes that overlap explicit — and it gives you the results in a predictable array when every Promise has finished.

One caveat: Promise.all() rejects as soon as any Promise rejects. If you want each request to succeed or fail on its own, use Promise.allSettled() instead.

A rule of thumb you can remember

await pauses this function. It does not pause JavaScript.

If the next line needs the result, await it.

If two tasks can run at the same time, start both, then await Promise.all().

If you called an async function and did not await it, you only started it. The rest of your code will keep going.

That last sentence is the whole article in one line. The surprising logs are just that idea showing up on a timeline.

Related posts

Comments (0)

Join the discussion and share your thoughts on this post.

💬

No comments yet

Be the first to share your thoughts!