Skip to content

How async and await work

At a high level, async and await in JavaScript are syntax built on top of Promises. They let you write asynchronous code in a style that reads top-to-bottom.

What async does

Declaring a function as async means the function always returns a Promise.

  • Returned values are wrapped in a fulfilled Promise.
  • Thrown errors become a rejected Promise.
async function f() {
  return 42;
}

This is equivalent to:

function f() {
  return Promise.resolve(42);
}

If the function throws, the returned Promise is rejected:

async function f() {
  throw new Error("oops");
}

Equivalent to:

function f() {
  return Promise.reject(new Error("oops"));
}

What await does

await pauses the current async function until its operand settles.

const result = await somePromise;

Behavior:

  • If the awaited value fulfills, await evaluates to the fulfillment value.
  • If rejection occurs, await throws the rejection reason.

await also accepts non-Promise values. They are converted with Promise.resolve(...), so this is valid:

const x = await 5;  // x === 5

For error handling, use try/catch inside async functions:

try {
  const data = await fetchData();
} catch (err) {
  // handles rejection
}

Example: Promise vs async/await

Using Promises:

fetchData()
  .then(data => process(data))
  .then(result => console.log(result))
  .catch(err => console.error(err));

Using async/await:

async function run() {
  try {
    const data = await fetchData();
    const result = process(data);
    console.log(result);
  } catch (err) {
    console.error(err);
  }
}

Both versions express the same flow. The async/await form is usually easier to read and debug.

Important: no thread blocking

Even though await looks like a pause point, JavaScript remains non-blocking.

Under the hood:

  • The function pauses
  • Control returns to the event loop
  • Other code can run
  • When the Promise resolves, execution resumes

Execution flow (mental model)

When you hit an await:

  1. Evaluate the expression and convert the result to a Promise when needed.
  2. Suspend the function
  3. Return control to the event loop
  4. Resume the function when that Promise settles

Sequential vs parallel awaits

Sequential (often slower; runs one after another):

const a = await getA();
const b = await getB();

Parallel start (often faster):

const [a, b] = await Promise.all([getA(), getB()]);

Note: Promise.all is fail-fast. If any Promise rejects, the whole await Promise.all(...) throws.

Common pitfalls

Forgetting await:

const data = fetchData(); // still a Promise!

Using await outside async contexts:

await fetchData(); // SyntaxError in scripts (allowed in ES modules)

Accidental serialization:

for (const item of items) {
  await process(item); // runs one-by-one
}

When work is independent, parallelize:

await Promise.all(items.map(process));

Summary of mental model

  • async makes a function return a Promise.
  • await extracts a fulfillment value or throws on rejection.
  • await pauses only the current async function, not the JavaScript thread.
  • async/await is Promise-based syntax, not a different concurrency model.