Articles

Async/Await vs Promises in JavaScript

Async/await is syntactic sugar over Promises, not a different mechanism. How each looks in practice, and when to still reach for raw Promises.

Takina Takina · · 4 min read
A laptop displaying code on a blue-lit screen

Promises are JavaScript’s built-in representation of a value that isn’t available yet but will be, resolved through .then() and .catch() chains. Async/await is syntax built directly on top of Promises that lets you write asynchronous code that reads like synchronous code, without changing what’s actually happening underneath. Neither is a competing technology — async/await is Promises, just with different syntax for consuming them.

How Promises work

A Promise represents one of three states: pending, fulfilled, or rejected. You attach callbacks with .then() for success and .catch() for failure, and chain multiple asynchronous steps by returning a new Promise from inside a .then():

fetch("/api/user")
  .then((res) => res.json())
  .then((user) => fetch(`/api/orders/${user.id}`))
  .then((res) => res.json())
  .then((orders) => console.log(orders))
  .catch((err) => console.error(err));

This solved a real problem — deeply nested callbacks, sometimes called “callback hell,” where each async step indented further than the last. Promise chains flatten that nesting, and a single .catch() at the end handles errors from any step in the chain. Promises are built on the JavaScript event loop: the callback passed to .then() doesn’t run immediately, it’s queued as a microtask once the Promise settles.

How async/await works

Marking a function async means it always returns a Promise, and lets you use await inside it to pause execution at a Promise until it settles, without a .then() callback:

async function getOrders() {
  const userRes = await fetch("/api/user");
  const user = await userRes.json();
  const ordersRes = await fetch(`/api/orders/${user.id}`);
  return ordersRes.json();
}

This is the exact same underlying mechanism as the Promise chain above — await is pausing on a Promise and resuming when it resolves, just as .then() does — but it reads top to bottom like ordinary synchronous code, which is easier to follow once a function chains more than two or three async steps.

Under the hood, await doesn’t block the thread. It suspends the async function and lets other queued work run, resuming the function’s execution once the awaited Promise settles — the same non-blocking behavior Promises already had, presented with different syntax.

Error handling: try/catch vs .catch()

Promise chains handle errors with .catch(), which catches a rejection from anywhere earlier in the chain. Async/await uses ordinary try/catch, which reads more naturally alongside synchronous error handling you’d already write elsewhere:

async function getOrders() {
  try {
    const userRes = await fetch("/api/user");
    const user = await userRes.json();
    return await fetch(`/api/orders/${user.id}`);
  } catch (err) {
    console.error(err);
  }
}

One easy mistake: forgetting await before a function call that returns a rejected Promise inside a try block means the rejection never actually reaches the catch — the try block only catches errors from operations it actually awaited.

Running things in parallel

A common performance mistake with async/await is awaiting independent operations one after another, which serializes work that didn’t need to be serial:

// Sequential — waits for each before starting the next
const a = await fetchA();
const b = await fetchB();

// Parallel — both start immediately
const [a, b] = await Promise.all([fetchA(), fetchB()]);

Promise.all() (and its relatives, Promise.allSettled() for when you want results even if some reject, and Promise.race() for the first to settle) is still the right tool for running independent async operations concurrently — async/await doesn’t replace these APIs, it just gives you a cleaner way to consume their results once they resolve.

Async/await vs Promises

Promises (.then()/.catch())Async/await
Underlying mechanismSame — both use the Promise objectSame — both use the Promise object
Reads asChained callbacksSequential, synchronous-looking code
Error handling.catch()try/catch
Best for simple, single-step asyncConcise for one-off .then() callsSlightly more verbose for a single call
Best for multi-step asyncNesting grows with each stepStays flat regardless of step count
Running tasks in parallelPromise.all() directlyStill needs Promise.all(), awaited
Function signatureAny function can return a PromiseRequires the async keyword

When to still reach for raw Promises

Async/await is generally easier to read for multi-step sequences, but plain Promise methods are still the right choice for functional-style chaining, for libraries that expose a Promise-returning API you’re just passing through, or for coordinating a set of concurrent operations with Promise.all() — which you’ll typically call with await in front of it anyway rather than treating the two as alternatives. In practice, most modern JavaScript and TypeScript code uses async/await for control flow and drops down to Promise combinators specifically when it needs to run things concurrently.

The takeaway

Async/await doesn’t replace Promises — it’s syntax layered on top of the same mechanism, designed to make multi-step asynchronous code read like ordinary sequential code and handle errors with familiar try/catch blocks. Use await for readability in sequential flows, but don’t forget Promise.all() when operations are actually independent — awaiting them one by one is a common and easy-to-miss performance regression.

Takina Takina · · 4 min read

Understanding the JavaScript Event Loop

JavaScript is single-threaded, yet pages stay responsive. A clear tour of the call stack, task queue, and microtasks — with examples you can run.

#JavaScript #Web Development #Async
Takina Takina · · 4 min read

TypeScript Abstract Classes, Explained

Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.

#TypeScript #JavaScript #Web Development
Takina Takina · · 4 min read

What Is a Lockfile? Reproducible Dependency Installs

A lockfile records the exact dependency versions your package manager resolved, so every install — from your laptop to CI — reproduces the same tree.

#JavaScript #Web Development #Developer Tools