LLevelUp
0
← Back to topic

Promises & async/await

Asynchronous code is at the heart of JavaScript: fetching data, reading files, setting timers. Promises replaced callback hell, and async/await made asynchronous code read almost like synchronous code — without blocking the thread.

Promise States

A Promise is an object representing a value that may not be available yet. It is always in exactly one of three states:

stateDiagram-v2
    [*] --> Pending
    Pending --> Fulfilled: resolve(value)
    Pending --> Rejected: reject(reason)
    Fulfilled --> [*]
    Rejected --> [*]

Once a Promise transitions from Pending to either Fulfilled or Rejected, it is settled — it can never change state again.

Creating Promises

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

function fetchUser(id: number): Promise<{ name: string }> {
  return new Promise((resolve, reject) => {
    if (id <= 0) {
      reject(new Error("Invalid ID"));
      return;
    }
    // Simulate async work
    setTimeout(() => resolve({ name: "Alice" }), 100);
  });
}

.then / .catch / .finally

Each .then returns a new Promise, enabling chaining. The value returned from a .then callback becomes the resolved value of the next Promise in the chain.

fetchUser(1)
  .then((user) => {
    console.log(user.name); // "Alice"
    return user.name.toUpperCase();
  })
  .then((upper) => {
    console.log(upper); // "ALICE"
  })
  .catch((err) => {
    // Catches rejections from any step above
    console.error(err.message);
  })
  .finally(() => {
    // Runs regardless of success or failure
    console.log("Done");
  });

WARNING

A .catch at the end of a chain only catches errors if they propagate there. An error thrown inside a .then that has its own try/catch will not reach the outer .catch.

async/await

async functions always return a Promise. Inside them, await pauses execution of the function (without blocking the thread) until the awaited Promise settles:

async function loadProfile(userId: number): Promise<string> {
  try {
    const user = await fetchUser(userId);
    await delay(50); // wait 50ms
    return `Profile: ${user.name}`;
  } catch (err) {
    if (err instanceof Error) {
      throw new Error(`Could not load profile: ${err.message}`);
    }
    throw err;
  }
}

NOTE

await can only appear inside an async function (or at the top level of an ES module with top-level await support). Using it elsewhere is a syntax error.

Promise Coordination

Promise.all — all must succeed

Runs all Promises concurrently. Resolves when all settle with fulfilled, or rejects immediately if any rejects.

const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
]);
// Both fetches ran in parallel

Promise.race — first settled wins

Resolves or rejects as soon as the first Promise settles, in either direction.

// Timeout pattern
const result = await Promise.race([
  fetchData(),
  delay(5000).then(() => { throw new Error("Timeout"); }),
]);

Promise.allSettled — wait for all, never rejects

Resolves with an array of outcome objects — useful when you want all results regardless of individual failures.

const results = await Promise.allSettled([fetchUser(1), fetchUser(-1)]);

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log(result.value);
  } else {
    console.error(result.reason);
  }
}

Common Async Mistakes

Floating Promise — forgetting to await or chain .catch, causing unhandled rejections:

// Bug: fetchUser rejection goes unhandled
fetchUser(id); // floating — result and errors ignored

// Fix
await fetchUser(id);
// or
void fetchUser(id).catch(handleError);

Sequential awaits that could be parallel:

// Slow: waits for each before starting the next
const a = await fetchA(); // 100ms
const b = await fetchB(); // 100ms after a finishes → total 200ms

// Fast: both start immediately
const [a, b] = await Promise.all([fetchA(), fetchB()]); // ~100ms total

CAUTION

async/await makes code look synchronous — this is intentional, but it can mask performance issues. Always ask whether awaits in a loop or in sequence actually need to be sequential.

Further Learning

Search these terms to go deeper:

  • “MDN: Using Promises” — thorough guide including microtask queue mechanics
  • “JavaScript Promises: an Introduction” on web.dev by Jake Archibald — excellent intuition-building article
  • “async/await under the hood” — articles explaining how async functions compile to generator/state machine code
  • “JavaScript event loop” — Philip Roberts’ “What the heck is the event loop anyway?” (JSConf EU talk) is the canonical starting point
  • “Promise.any and AggregateError” — the fourth Promise combinator, useful for “first success” patterns