# How Async/Await Functions Work Under the Hood in JavaScript

> Uncover how async await functions work under the hood in JavaScript. Learn how they compile to state machines and interact with Promises for efficient asynchronous operation.

- Repository: [Leonardo Maldonado/33-js-concepts](https://github.com/leonardomso/33-js-concepts)
- Tags: deep-dive
- Published: 2026-03-03

---

**Async/await functions are syntactic sugar over Promises that compile to state machines, automatically wrapping return values in Promises and pausing execution at each `await` until the awaited expression settles.**

The `leonardomso/33-js-concepts` repository provides an exhaustive test suite in [`tests/functions-execution/async-await/async-await.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/async-await/async-await.test.js) that demonstrates exactly how the JavaScript engine transforms `async` function declarations into Promise-based execution flows. Understanding this transformation is essential for mastering modern asynchronous programming patterns.

## The Promise Foundation

`async`/`await` does not introduce new concurrency primitives to JavaScript. Instead, it provides a **synchronous-looking syntax** for code that ultimately operates on **Promises**. When the JavaScript parser encounters an `async` function, it rewrites the body into a state machine that chains Promise callbacks.

As implemented in the `33-js-concepts` test suite, every `async` function exhibits five core behaviors that reveal its Promise-based nature: automatic Promise wrapping, await pausing, error transformation, avoidance of double-wrapping, and thenable support.

## Automatic Promise Wrapping

An `async` function **always returns a Promise**, regardless of what you explicitly return. The engine automatically wraps the return value using `Promise.resolve()`.

The test **"should make a function return a Promise"** in [`async-await.test.js:10-19`](https://github.com/leonardomso/33-js-concepts/blob/master/tests/functions-execution/async-await/async-await.test.js#L10-L19) demonstrates this behavior. Even when you return a primitive, the caller receives a Promise:

```javascript
async function fetchNumber() {
  return 42;  // Compiled to: return Promise.resolve(42);
}

fetchNumber().then(v => console.log(v));  // → 42

```

This automatic wrapping ensures that `async` functions maintain a consistent contract: they always return thenable objects that can be chained with `.then()` and `.catch()`.

## The Await Pause Mechanism

The `await` keyword triggers the state machine to **pause execution** until the awaited expression settles. Critically, the test **"code before await is synchronous"** in [`async-await.test.js:77-100`](https://github.com/leonardomso/33-js-concepts/blob/master/tests/functions-execution/async-await/async-await.test.js#L77-L100) proves that code before the first `await` runs immediately, while subsequent code queues as a micro-task.

Under the hood, the engine compiles the function into a chain of `.then()` callbacks:

```javascript
async function foo() {
  // Compiled approximately to:
  return Promise.resolve()
    .then(() => expr1)          // Evaluate before first await
    .then(val => { /* resume */ })
    .then(() => expr2);         // Second await, etc.
}

```

This compilation strategy keeps the call stack shallow while maintaining the illusion of sequential execution:

```javascript
async function demoOrder() {
  console.log('before');        // Runs immediately (synchronous)
  await Promise.resolve();      // Creates micro-task pause
  console.log('after');         // Runs after current tick
}

demoOrder();  // Logs "before", then "after" in next micro-task

```

## Error Handling Transformation

`async` functions transform **synchronous throws into rejected Promises**. If an `async` function throws an exception, the engine catches it internally and rejects the returned Promise with the thrown value.

The test **"should convert thrown errors to rejected Promises"** in [`async-await.test.js:32-40`](https://github.com/leonardomso/33-js-concepts/blob/master/tests/functions-execution/async-await/async-await.test.js#L32-L40) verifies this behavior:

```javascript
async function mayFail(flag) {
  if (flag) throw new Error('boom');
  return 'ok';
}

mayFail(true).catch(e => console.error(e.message));  // → "boom"

```

This design eliminates the distinction between synchronous failures and asynchronous rejections, allowing a single `.catch()` handler to manage all error scenarios.

## Thenable Support and Avoiding Double-Wrapping

The engine optimizes for cases where you explicitly return a Promise. The test **"should not double-wrap returned Promises"** in [`async-await.test.js:41-52`](https://github.com/leonardomso/33-js-concepts/blob/master/tests/functions-execution/async-await/async-await.test.js#L41-L52) confirms that `Promise.resolve(42)` returned from an `async` function stays a single Promise rather than becoming `Promise.resolve(Promise.resolve(42))`.

Additionally, `await` works with **any thenable**—objects implementing a `then` method. The test **"should work with thenable objects"** in [`async-await.test.js:25-38`](https://github.com/leonardomso/33-js-concepts/blob/master/tests/functions-execution/async-await/async-await.test.js#L25-L38) demonstrates that custom thenables behave like native Promises:

```javascript
const customThenable = {
  then(resolve) { resolve(42); }
};

async function consumeThenable() {
  const value = await customThenable;  // Calls .then()
  return value;
}

```

## Sequential vs. Parallel Execution Patterns

Understanding the compilation model reveals why `await` inside loops creates sequential execution while `Promise.all` enables parallelism. The `33-js-concepts` repository tests both patterns in **"Sequential vs Parallel Execution"** (`async-await.test.js:71-124`).

**Sequential execution** waits for each Promise to settle before starting the next:

```javascript
async function sequential() {
  const a = await delay(100, 'A');  // Wait 100ms
  const b = await delay(100, 'B');  // Wait another 100ms
  return [a, b];                    // Total: 200ms
}

```

**Parallel execution** starts all operations immediately and awaits the aggregated Promise:

```javascript
async function parallel() {
  const [a, b] = await Promise.all([
    delay(100, 'A'),  // Starts immediately
    delay(100, 'B')   // Starts immediately
  ]);
  return [a, b];       // Total: ~100ms
}

```

## Interoperability with Promise Chains

`async`/`await` compiles to standard Promise chains, enabling seamless mixing of syntax styles. The test **"should allow mixing async/await and Promise chains"** in [`async-await.test.js:71-89`](https://github.com/leonardomso/33-js-concepts/blob/master/tests/functions-execution/async-await/async-await.test.js#L71-L89) demonstrates this interoperability:

```javascript
async function step1() { return 1; }
function step2(v) { return Promise.resolve(v + 1); }
async function step3(v) { return v + 1; }

const result = await step1()
  .then(step2)   // Returns Promise
  .then(step3);  // Async function, returns Promise
// result === 3

```

## Summary

- **Syntactic sugar**: `async`/`await` compiles to Promise-based state machines, not new concurrency primitives.
- **Always Promise**: `async` functions automatically wrap return values with `Promise.resolve()` according to [`async-await.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/async-await.test.js).
- **Pause points**: Code before the first `await` runs synchronously; subsequent code queues as micro-tasks.
- **Error normalization**: Thrown exceptions become rejected Promises, unifying error handling between sync and async code.
- **Thenable flexibility**: `await` accepts any object with a `then` method, not just native Promises.
- **Execution control**: Sequential `await` calls chain sequentially; `Promise.all` enables parallel execution within `async` functions.

## Frequently Asked Questions

### Does async/await replace Promises in JavaScript?

No, `async`/`await` is **syntactic sugar** built on top of Promises. According to the `33-js-concepts` source code, the JavaScript engine rewrites `async` functions into state machines that chain `.then()` callbacks. You cannot use `await` without a Promise-based runtime, and `async` functions always return Promise objects.

### Why does code before the first await run immediately?

The JavaScript engine evaluates the expression before the first `await` as part of the synchronous function entry. As demonstrated in `async-await.test.js:77-100`, only the code *after* the `await` keyword gets deferred to the micro-task queue. This design allows `async` functions to perform synchronous setup before yielding control to the event loop.

### What happens if I throw an error inside an async function?

The JavaScript engine automatically **catches synchronous throws** and converts them into rejected Promises. When you `throw new Error()` inside an `async` function, the returned Promise enters the rejected state, allowing callers to handle the error using `.catch()` or try/catch blocks in parent `async` functions.

### Can I await non-Promise values?

Yes. The `await` operator accepts **any thenable**—objects with a `then` method—or non-thenable values. According to `async-await.test.js:25-38`, if you `await` a regular value like `42`, the engine wraps it in `Promise.resolve(42)` and resolves immediately. If you await a custom thenable, the engine invokes its `then` method to retrieve the resolution value.