DEV Community

Cover image for What Does `async/await` Actually Do in JavaScript?
Aditya Sharma
Aditya Sharma

Posted on

What Does `async/await` Actually Do in JavaScript?

Most developers learn async/await as syntax that makes asynchronous code look synchronous. You await something, and the code below it runs after. It reads top to bottom, like normal code. That mental model is mostly fine for writing async functions, but it hides what's actually happening. And once you need to debug a weird execution order or understand why something runs when it does, the surface-level model breaks down.

The thing most people get wrong: await looks like it pauses execution. It doesn't. Not in the way you might think.


An async Function Is Just a Function That Returns a Promise

Start here, because everything else follows from it.

async function getData() {
    return 42;
}
Enter fullscreen mode Exit fullscreen mode

getData() doesn't return 42. It returns Promise.resolve(42). Every async function wraps its return value in a Promise, whether you ask for it or not. If the function throws, the returned Promise rejects instead.

This matters because it means an async function, from the outside, is just a function that returns a Promise. The async keyword is doing two things: allowing await inside the function, and ensuring the return value is always a Promise.


What await Actually Does

When execution reaches an await expression, a few things happen.

async function getData() {
    console.log("A");

    const response = await fetch("/data");

    console.log("B");
}
Enter fullscreen mode Exit fullscreen mode

console.log("A") runs synchronously. Then fetch("/data") is called, which returns a Promise. At the point of await, the async function suspends. Execution leaves getData and returns to whatever called it.

The key word is suspends, not blocks. The JavaScript thread is not sitting there waiting. It's free to do other things. The rest of the call stack continues. Other code can run. The event loop can process other tasks. getData is just paused at that line, waiting for the Promise to settle.

When the Promise from fetch eventually resolves, the continuation of getData, everything after the await- gets scheduled to run. The function picks up where it left off. console.log("B") runs.


The Thread Isn't Paused. The Function Is.

This is the distinction that matters most.

JavaScript is single-threaded. There is one call stack. When code is running, nothing else runs. await doesn't change this. It doesn't create a new thread. It doesn't run anything in the background. What it does is hand control back to the event loop while the current async function waits.

The event loop is part of the mechanism that allows JavaScript to process other scheduled work while an async function is suspended. It looks for work to do: callbacks from timers, I/O results, resolved Promises, user events. When a Promise that was being awaited settles, the continuation of the async function is scheduled as a microtask, and the event loop runs it once the current call stack is empty.

To see this clearly, look at what happens with an immediately resolved Promise:

async function test() {
    console.log("A");
    await Promise.resolve();
    console.log("B");
}

console.log("C");
test();
console.log("D");
Enter fullscreen mode Exit fullscreen mode

You might expect: C, A, B, D. The actual output is: C, A, D, B.

Here's why. console.log("C") runs. test() is called. Inside test, console.log("A") runs. Then await Promise.resolve() is reached. The Promise is already resolved, but await still suspends the function. The continuation is scheduled as a microtask, not run immediately. Execution returns to the caller. console.log("D") runs. The current call stack is now empty. The event loop picks up the microtask. console.log("B") runs.

Even though the Promise was resolved instantly, the code after await didn't run synchronously. The continuation is always scheduled as a microtask rather than continuing inline. That's by design, and it keeps async functions behaviorally consistent regardless of whether the awaited value was ready immediately or not.


await Doesn't Make Things Run in Parallel

This is a common misunderstanding worth being explicit about.

const a = await fetch("/endpoint-a");
const b = await fetch("/endpoint-b");
Enter fullscreen mode Exit fullscreen mode

These two requests don't run at the same time. The second fetch doesn't start until the first one resolves. You've written sequential async code, not parallel async code. If you want both requests to happen concurrently, you have to start both Promises before awaiting either:

const [a, b] = await Promise.all([fetch("/endpoint-a"), fetch("/endpoint-b")]);
Enter fullscreen mode Exit fullscreen mode

await means "suspend this async function until this Promise settles." Whether things run in parallel depends entirely on when you start them, not on how many await keywords you use.


Where the Asynchronous Work Actually Happens

One more thing worth clarifying: when you await fetch("/data"), the HTTP request isn't happening inside JavaScript. The fetch call hands the request to the browser's networking layer, which handles it outside the JavaScript thread. JavaScript just registers interest in the result. When the response arrives, the browser resolves the Promise, and the event loop delivers the continuation back to your async function.

This is why await works without blocking the thread. The actual waiting isn't happening in JavaScript at all. It's happening in the host environment. JavaScript just picks up the result when it's ready.


async/await is a cleaner way to work with Promises. It makes the control flow easier to follow and the error handling more natural. But the execution model underneath it is the same: Promises, microtasks, and the event loop.

await doesn't pause JavaScript. It pauses the function you're currently inside, hands control back to the event loop, and lets the thread get on with other work until the Promise settles and your function is ready to continue.

The code looks like it stops and waits. The runtime doesn't.

Top comments (1)

Collapse
 
systemcraftdev profile image
SystemCraftDev

Really clean breakdown, especially the C, A, D, B walkthrough — that's the exact example that untangles the "await doesn't run synchronously even for an already-resolved promise" confusion.

One gotcha worth flagging in the parallel-vs-sequential section: await inside .forEach(). Since forEach doesn't wait on its callback's return value, this looks sequential but actually fires everything and moves on immediately:

items.forEach(async (item) => {
  await doSomething(item);
});
console.log('done'); // runs before any doSomething() finishes
Enter fullscreen mode Exit fullscreen mode

for...of with await inside gives you the sequential behavior you'd expect; Promise.all(items.map(...)) gives you the parallel version. Easy to miss since nothing errors — it just silently doesn't wait.