Async/Await vs Promises: The JavaScript Story Nobody Tells You

Async/Await vs Promises: The JavaScript Story Nobody Tells You

Async/Await vs Promises: The JavaScript Story Nobody Tells You

Promises and async/await are often framed as competitors — but they’re actually teammates. This story-driven guide explains their relationship, shows practical examples, and highlights common beginner mistakes with clear fixes.

By ·

1. The Callback Era (The Problem)

When I first learned JavaScript promises, I was thrilled. Before promises, asynchronous code looked like nested callbacks — famously known as callback hell. Readability suffered and error handling was clumsy.

// callback hell example
function getUser(callback) {
  setTimeout(() => {
    callback(null, { id: 1, name: "Alice" });
  }, 1000);
}

function getOrders(userId, callback) {
  setTimeout(() => {
    callback(null, ["Order1", "Order2"]);
  }, 1000);
}

getUser((err, user) => {
  if (err) return console.error(err);
  getOrders(user.id, (err, orders) => {
    if (err) return console.error(err);
    console.log("Orders:", orders);
  });
});
Problem: deeply nested callbacks are hard to read and test.

2. Promises to the Rescue

Promises flattened that nesting and gave us a chainable API.

// Promises example
function getUser() {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id: 1, name: "Alice" }), 1000);
  });
}

function getOrders(userId) {
  return new Promise(resolve => {
    setTimeout(() => resolve(["Order1", "Order2"]), 1000);
  });
}

getUser()
  .then(user => getOrders(user.id))
  .then(orders => console.log("Orders:", orders))
  .catch(err => console.error(err));
Upside: cleaner flow and `.catch()` for centralized error handling. Downside: long chains still become hard to manage when logic grows.

3. The Async/Await Revolution

Introduced in ES2017, async/await is syntactic sugar over promises. It lets you write asynchronous code that looks synchronous — making it easier to reason about.

// async/await example
async function fetchOrders() {
  try {
    const user = await getUser();
    const orders = await getOrders(user.id);
    console.log("Orders:", orders);
  } catch (err) {
    console.error(err);
  }
}

fetchOrders();
Although it looks synchronous, it remains asynchronous under the hood.

4. The Truth Nobody Tells You

Async/await does not replace promises — it builds on them. `await` expects a promise; `async` functions always return a promise.

// async returns a promise
async function hello() {
  return "Hello World";
}

hello().then(msg => console.log(msg)); // "Hello World"
Use whichever syntax makes your code clearer — remember they interoperate seamlessly.

5. When to Use Promises vs Async/Await

Both are useful. Choose based on parallelism vs sequential flow.

Use Promises (and Promise.all) when:

  • You have multiple independent async tasks and want them to run in parallel.
  • You prefer a pipeline-style chain for transforms.
// Promise.all example — parallel fetches
Promise.all([getUser(), getOrders(1)])
  .then(([user, orders]) => console.log(user, orders))
  .catch(console.error);

Use Async/Await when:

  • Tasks are sequential and depend on previous results.
  • You want clearer try/catch style error handling.
// sequential with async/await
async function main() {
  const user = await getUser();
  const orders = await getOrders(user.id);
  console.log(user, orders);
}

6. Mixing Both (The Secret Sauce)

In real projects you’ll mix patterns. A common idiom: use async/await for top-level readability, and Promise.all for parallelism within it.

// best of both: Promise.all inside async/await
async function main() {
  const user = await getUser();
  const [orders, profile] = await Promise.all([
    getOrders(user.id),
    getProfile(user.id)
  ]);
  console.log(orders, profile);
}
This gives readable control flow while keeping parallelism where it matters.

7. Common Mistakes Beginners Make

Forgetting to await

// ❌ missing await -> returns a Promise
async function main() {
  const user = getUser(); // missing await
  console.log(user); // Promise {  }
}
// ✅ include await when you need the result
async function main() {
  const user = await getUser();
  console.log(user);
}

Awaiting in a loop (slow)

// ❌ sequential (slow)
for (let id of [1,2,3]) {
  const user = await getUser(id);
  console.log(user);
}
// ✅ parallel using map + Promise.all
const promises = [1,2,3].map(id => getUser(id));
const users = await Promise.all(promises);
console.log(users);

Not handling rejection in Promise.all

// ❌ one reject breaks all
await Promise.all([p1, p2, p3]); // if p2 rejects, Promise.all rejects
// ✅ handle individually (settled results)
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(r => {
  if (r.status === 'fulfilled') console.log('ok', r.value);
  else console.warn('failed', r.reason);
});
Use Promise.allSettled or guard individual promises when you need fault tolerance.

8. Final Thoughts

Callbacks were messy, Promises made async manageable, and async/await made it readable. None of them are enemies. Use promises for parallelism, async/await for clarity, and mix both when appropriate.

If you enjoyed this, follow me for daily dev insights on JavaScript, Python, and Cloud — and tell me: which async pattern do you use most?

Follow me on Medium

Suggested tags: JavaScript, Async/Await, Promises, Web Development, Programming

Comments

Popular posts from this blog

Docker for Beginners: Build, Ship, and Run Apps with Ease

Top 7 Real-World Projects to Learn React, Python, and AWS (Beginner to Advanced)