DEV Community

Cover image for Why await Doesn't Make JavaScript Run in Order
Jay @ Designly
Jay @ Designly

Posted on • Originally published at blog.designly.biz

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:

await doSomething();
// nothing else in the entire program can happen until this finishes
Enter fullscreen mode Exit fullscreen mode

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.

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');
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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.

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

loadUser();
// this line does not wait
Enter fullscreen mode Exit fullscreen mode

loadUser() is paused. The caller is not.

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

await loadUser();
await loadSettings();
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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

  return { user, projects };
}
Enter fullscreen mode Exit fullscreen mode

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:

await createCustomer(form);
await sendWelcomeEmail(form.email);
Enter fullscreen mode Exit fullscreen mode

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:

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

  return { user, settings, notifications };
}
Enter fullscreen mode Exit fullscreen mode

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:

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

  return { user, settings, notifications };
}
Enter fullscreen mode Exit fullscreen mode

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.


This story was originally published at blog.designly.biz on August 16, 2026.

Top comments (0)