DEV Community

Cover image for Deep Dive: What Actually Runs When You Write 'await' in JavaScript?
Adam - The Developer
Adam - The Developer

Posted on

Deep Dive: What Actually Runs When You Write 'await' in JavaScript?

Ever wondered what's really happening when you write async and await in JavaScript? Let's peel back the layers and explore this feature from the high-level syntax all the way down to the C++ implementation in V8.

Table of Contents

  1. The Surface: What You Write
  2. Level 1: Desugaring to Promises
  3. Level 2: Generator Functions
  4. Level 3: JavaScript Runtime & Microtasks
  5. Level 4: V8's C++ Implementation
  6. Putting It All Together

1. The Surface: What You Write

Let's start with familiar async/await code:

async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  return data;
}
Enter fullscreen mode Exit fullscreen mode

This looks synchronous but behaves asynchronously. Magic? Not quite.


2. Level 1: Desugaring to Promises

The async/await syntax is syntactic sugar over Promises. Here's what your async function really looks like:

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    fetch(`/api/users/${userId}`)
      .then(response => {
        return response.json();
      })
      .then(data => {
        resolve(data);
      })
      .catch(error => {
        reject(error);
      });
  });
}
Enter fullscreen mode Exit fullscreen mode

Key Transformations:

  • async function → returns a Promise automatically
  • await expression.then() on the promise
  • return valueresolve(value)
  • Thrown errors → reject(error)

But this doesn't explain how the function "pauses" at each await. That's where generators come in.


3. Level 2: Generator Functions

Under the hood, async functions are implemented using generator functions. Here's a closer approximation:

function* fetchUserDataGenerator(userId) {
  const response = yield fetch(`/api/users/${userId}`);
  const data = yield response.json();
  return data;
}

// Wrapped in a runner
function asyncToGenerator(generatorFunc) {
  return function(...args) {
    const gen = generatorFunc(...args);

    return new Promise((resolve, reject) => {
      function step(nextFunc) {
        let result;
        try {
          result = nextFunc();
        } catch (e) {
          reject(e);
          return;
        }

        if (result.done) {
          resolve(result.value);
          return;
        }

        // Convert to Promise and continue
        Promise.resolve(result.value).then(
          value => step(() => gen.next(value)),
          error => step(() => gen.throw(error))
        );
      }

      step(() => gen.next());
    });
  };
}

const fetchUserData = asyncToGenerator(fetchUserDataGenerator);
Enter fullscreen mode Exit fullscreen mode

This is essentially what transpilers like Babel do when targeting older JavaScript versions.

How Generators Enable Pausing:

Generators have the ability to:

  • Yield control back to the caller with yield
  • Resume execution when .next() is called
  • Maintain state between yields

This is the mechanism that allows async functions to "pause" at await points.


4. Level 3: JavaScript Runtime & Microtasks

When you await a Promise, here's what happens in the JavaScript event loop:

1. Hit await → Promise not resolved yet
2. Suspend function execution
3. Return control to event loop
4. When Promise resolves → Queue microtask
5. Microtask runs → Resume function execution
Enter fullscreen mode Exit fullscreen mode

The Microtask Queue:

console.log('1: Synchronous');

async function example() {
  console.log('2: Async function start');
  await Promise.resolve();
  console.log('4: After await');
}

example();

console.log('3: Synchronous end');

// Output:
// 1: Synchronous
// 2: Async function start
// 3: Synchronous end
// 4: After await
Enter fullscreen mode Exit fullscreen mode

The "After await" runs in a microtask, which executes after the current synchronous code but before the next task (like setTimeout callbacks).


5. Level 4: V8's C++ Implementation

Now let's dive into V8, Chrome's JavaScript engine written in C++.

A. Async Function Objects

In V8, async functions are represented by JSAsyncFunctionObject. When you call an async function, V8:

// Simplified from v8/src/builtins/builtins-async-gen.cc

// When entering an async function
BUILTIN(AsyncFunctionEnter) {
  // Create a new promise (the one the async function returns)
  Handle<JSPromise> promise = factory->NewJSPromise();

  // Create generator-like context to store state
  Handle<Context> context = factory->NewAsyncFunctionContext();

  // Store the promise for later resolution
  context->set_extension(*promise);

  return *promise;
}
Enter fullscreen mode Exit fullscreen mode

B. Await Implementation

The await keyword is handled by the Await builtin:

// Simplified from v8/src/builtins/builtins-async-gen.cc

BUILTIN(AsyncFunctionAwait) {
  // Get the awaited value
  Handle<Object> value = args.at(1);

  // Get the implicit promise (async function's return promise)
  Handle<JSAsyncFunctionObject> async_function_object = args.at(0);
  Handle<JSPromise> outer_promise = async_function_object->promise();

  // Create a promise for the awaited value
  Handle<JSPromise> promise = factory->NewJSPromiseWithoutHook();

  // Resolve it with the awaited value
  ResolvePromise(isolate, promise, value);

  // Create PromiseReaction objects (the continuation)
  Handle<JSFunction> on_fulfilled = CreateAsyncFunctionResumeFunction();
  Handle<JSFunction> on_rejected = CreateAsyncFunctionRejectFunction();

  // Attach reactions to the promise
  PerformPromiseThen(isolate, promise, on_fulfilled, on_rejected, outer_promise);

  // Suspend execution (return to event loop)
  return ReadOnlyRoots(isolate).undefined_value();
}
Enter fullscreen mode Exit fullscreen mode

C. Resuming After Await

When the promise resolves, V8 queues a microtask:

// Simplified from v8/src/objects/js-promise.cc

void EnqueueMicrotask(Isolate* isolate, Handle<Microtask> microtask) {
  // Get the microtask queue
  Handle<MicrotaskQueue> queue = isolate->default_microtask_queue();

  // Add to the queue
  queue->EnqueueMicrotask(*microtask);

  // Notify that there are pending microtasks
  isolate->SetHasPendingMicrotasks();
}

// When it's time to run microtasks
void RunMicrotasks(Isolate* isolate) {
  HandleScope scope(isolate);
  MicrotaskQueue* queue = isolate->default_microtask_queue();

  while (queue->size() > 0) {
    Microtask* microtask = queue->Dequeue();
    microtask->Run(isolate);
  }
}
Enter fullscreen mode Exit fullscreen mode

D. Generator State Machine

V8 maintains a state machine for each async function:

enum class AsyncFunctionState {
  kSuspendedStart,    // Before first await
  kSuspendedYield,    // Suspended at await
  kExecuting,         // Currently running
  kCompleted          // Finished
};

// Stored in the function's context
struct AsyncFunctionData {
  AsyncFunctionState state;
  int resume_offset;        // Where to resume in bytecode
  Handle<Object> awaited_value;  // Value to resume with
  Handle<JSPromise> promise;     // Return promise
};
Enter fullscreen mode Exit fullscreen mode

E. Bytecode Level

V8's Ignition interpreter generates special bytecode for async functions:

// Simplified bytecode for: await somePromise()

SuspendGenerator  // Save state and yield control
Return            // Exit function temporarily

// --- Execution resumes here when promise resolves ---
ResumeGenerator   // Restore state
GetGeneratorResumeValue  // Get the resolved value
// Continue with next operations...
Enter fullscreen mode Exit fullscreen mode

6. Putting It All Together

Let's trace through a complete example:

async function example() {
  console.log('Before await');
  const result = await Promise.resolve(42);
  console.log('After await:', result);
  return result * 2;
}

const promise = example();
console.log('Function called');
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Execution:

1. Call example()

  • V8 creates JSAsyncFunctionObject
  • Creates implicit return promise
  • Starts synchronous execution

2. console.log('Before await')

  • Executes synchronously

3. Hit await Promise.resolve(42)

  • V8 AsyncFunctionAwait builtin called
  • Creates promise reaction (continuation function)
  • Suspends function (saves state in generator context)
  • Returns to caller

4. console.log('Function called')

  • Main execution continues

5. Promise Resolves

  • V8 enqueues microtask with resolved value (42)

6. Microtask Queue Runs

  • Calls continuation function
  • Restores async function state
  • Resumes execution at resume point

7. console.log('After await:', 42)

  • Executes with resumed value

8. return 84

  • Resolves implicit return promise with 84

Performance Implications

Understanding the implementation reveals why certain patterns are faster:

❌ Slower (unnecessary await):

async function slow() {
  return await someAsyncOperation();
}
// Creates extra promise + microtask
Enter fullscreen mode Exit fullscreen mode

✅ Faster (direct return):

async function fast() {
  return someAsyncOperation();
}
// Returns promise directly
Enter fullscreen mode Exit fullscreen mode

❌ Sequential (slower):

async function sequential() {
  const a = await fetchA();
  const b = await fetchB();
  return [a, b];
}
Enter fullscreen mode Exit fullscreen mode

✅ Parallel (faster):

async function parallel() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  return [a, b];
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Async/await is syntactic sugar over Promises and generator functions
  2. Generators provide the pause/resume mechanism through yield points
  3. The event loop and microtask queue handle scheduling
  4. V8 implements this in C++ with special objects, builtins, and bytecode
  5. Each await creates overhead - a promise wrapper and microtask

Understanding these layers helps you:

  • Write more performant async code
  • Debug async issues more effectively
  • Make informed architectural decisions
  • Appreciate the complexity hidden behind simple syntax

Further Reading

Top comments (0)