JavaScript

JavaScript Promises and Async/Await Explained

Stop fearing asynchronous JavaScript. Learn how Promises actually work under the hood, then master async/await with real-world examples and common pitfalls.

June 17, 20269 min read
Share
Advertisement (not configured)

Introduction

Asynchronous code trips up almost every JavaScript developer at some point — callback hell, unhandled rejections, race conditions. Promises and async/await exist to solve exactly this, but most tutorials skip why they work, not just how to use them.

In this guide, we'll cover:

  1. What a Promise actually is
  2. Creating and chaining Promises
  3. async/await syntax
  4. Error handling
  5. Running multiple Promises in parallel
  6. Common mistakes to avoid

Every example is something you'll actually write in real code.

1. What a Promise Actually Is

A Promise is an object representing a value that isn't available yet. It has three states: pending, fulfilled, or rejected — and once it settles, it never changes state again.

const promise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {
      resolve("Data loaded");
    } else {
      reject(new Error("Failed to load data"));
    }
  }, 1000);
});

console.log(promise); // Promise { <pending> }

resolve and reject are functions you call to settle the promise. Whatever you pass to them becomes the value that consumers receive.

2. Creating and Chaining Promises

Consume a Promise with .then() and .catch():

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id <= 0) {
        reject(new Error("Invalid user ID"));
        return;
      }
      resolve({ id, name: "Ali Khan" });
    }, 500);
  });
}

fetchUser(1)
  .then((user) => {
    console.log(user.name);
    return fetchUser(2); // chain another promise
  })
  .then((user) => console.log(user.name))
  .catch((error) => console.error(error.message));

Each .then() returns a new Promise, which is what makes chaining work. If you return a value, the next .then() receives it. If you return a Promise, the chain waits for it to settle first.

3. async/await Syntax

async/await is syntax sugar over Promises — it lets you write asynchronous code that reads like synchronous code:

async function getUserName(id) {
  const user = await fetchUser(id);
  return user.name;
}

getUserName(1).then((name) => console.log(name));

Two rules to remember:

1. `await` can only be used inside a function marked `async`.
2. An `async` function always returns a Promise — even if you
   `return` a plain value, it gets wrapped automatically.

Compare the same logic written both ways:

// Promise chain
function loadDashboard(userId) {
  return fetchUser(userId)
    .then((user) => fetchOrders(user.id))
    .then((orders) => fetchTotal(orders))
    .catch((err) => console.error(err));
}

// async/await — same behavior, easier to read
async function loadDashboard(userId) {
  try {
    const user = await fetchUser(userId);
    const orders = await fetchOrders(user.id);
    const total = await fetchTotal(orders);
    return total;
  } catch (err) {
    console.error(err);
  }
}

4. Error Handling

With Promises, errors propagate down the chain until a .catch() handles them. With async/await, use a standard try/catch block:

async function safeFetch(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error("Fetch failed:", error.message);
    return null;
  }
}

A common mistake is forgetting that a rejected Promise without a .catch() (or try/catch) crashes Node.js processes and logs unhandled rejection warnings in browsers. Always handle errors at the boundary where the async call happens.

5. Running Multiple Promises in Parallel

Awaiting Promises one by one is slow when they don't depend on each other. Use Promise.all() to run them concurrently:

async function loadAllData() {
  const [users, products, orders] = await Promise.all([
    fetchUsers(),
    fetchProducts(),
    fetchOrders(),
  ]);

  return { users, products, orders };
}
Method Behavior
Promise.all() Resolves when all succeed; rejects immediately if any one fails
Promise.allSettled() Waits for all to finish, success or failure, and reports each result
Promise.race() Settles as soon as the first Promise settles, win or lose
Promise.any() Resolves as soon as the first one succeeds; ignores failures unless all fail

Use Promise.allSettled() when you want results even if some requests fail:

async function loadOptionalData() {
  const results = await Promise.allSettled([
    fetchUsers(),
    fetchAnalytics(), // might fail, that's ok
  ]);

  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log("Got:", result.value);
    } else {
      console.warn("Failed:", result.reason.message);
    }
  });
}

6. Common Mistakes to Avoid

Mistake 1 — awaiting in a loop when requests are independent:

// Slow — each request waits for the previous one
for (const id of userIds) {
  const user = await fetchUser(id);
  console.log(user.name);
}

// Fast — all requests fire at once
const users = await Promise.all(userIds.map((id) => fetchUser(id)));
users.forEach((user) => console.log(user.name));

Mistake 2 — forgetting that forEach doesn't wait for async callbacks:

// Wrong — forEach doesn't await, all logs fire before data loads
userIds.forEach(async (id) => {
  const user = await fetchUser(id);
  console.log(user.name);
});

// Right — use a for...of loop or Promise.all instead
for (const id of userIds) {
  const user = await fetchUser(id);
  console.log(user.name);
}

Mistake 3 — mixing .then() and await unnecessarily. Pick one style per function — mixing them makes control flow harder to follow.

Putting It All Together

Concept Use it for
new Promise() Wrapping callback-based or timer-based async work
.then() / .catch() Chaining async steps the traditional way
async/await Writing async code that reads top-to-bottom
try/catch Catching errors in async functions
Promise.all() Running independent async tasks in parallel
Promise.allSettled() Parallel tasks where some failures are expected

Promises aren't magic — they're just objects that track a future value and notify you when it's ready. async/await doesn't replace Promises, it just makes them easier to read. Once that clicks, async JavaScript stops being scary and starts being just another tool you reach for without thinking twice.

Advertisement (not configured)

Written by

Raretechsol

International software company specializing in Python and JavaScript. Passionate about automation, AI, and building practical web applications.

Related Articles