JavaScript Promises vs Async/Await — Complete Guide with Examples

JavaScript Promises vs Async/Await — Complete Guide with Examples

If you have been writing JavaScript for a while, you have probably run into both Promises and async/await. And if you are like most developers, you probably wondered at some point — are these two the same thing? Do I need to learn both? Which one is better?

Let me clear this up once and for all in plain English.

First, Why Does This Even Exist?

Back in the day, JavaScript handled asynchronous tasks using callbacks. You know, the classic "function inside a function inside a function" situation. It looked something like this:

getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      console.log(details);
    });
  });
});

This is what developers call callback hell. It is messy, hard to read, and an absolute nightmare to debug. So the JavaScript community came up with a better solution — Promises.

What Is a Promise?

A Promise is exactly what it sounds like. It is JavaScript's way of saying — "I don't have this value right now, but I promise I will get back to you."

A Promise has three possible states:

  • Pending — still waiting for the result
  • Fulfilled — got the result successfully
  • Rejected — something went wrong

Here is a simple Promise in action:

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId) {
        resolve({ id: userId, name: "Randhir", role: "Developer" });
      } else {
        reject(new Error("User ID is required"));
      }
    }, 1000);
  });
}

fetchUserData(1)
  .then(user => {
    console.log("Got user:", user.name);
    return user;
  })
  .then(user => {
    console.log("User role:", user.role);
  })
  .catch(error => {
    console.error("Something went wrong:", error.message);
  })
  .finally(() => {
    console.log("Done — whether it worked or not");
  });

This is much cleaner than callbacks. You chain .then() for success, .catch() for errors, and .finally() for anything that should run regardless of the result.

But here is the honest truth — once you start chaining multiple .then() calls, it starts getting messy again. Not as bad as callbacks, but still not the most natural thing to read.

Enter Async/Await

Async/await was introduced in ES2017 and it completely changed how most developers write asynchronous JavaScript. Here is the key thing to understand upfront:

Async/await is not a replacement for Promises. It is built on top of Promises. Under the hood, async/await is still using Promises. It just gives you a much cleaner way to write them.

Here is the same example from above, rewritten with async/await:

async function getUserData(userId) {
  try {
    const user = await fetchUserData(userId);
    console.log("Got user:", user.name);
    console.log("User role:", user.role);
  } catch (error) {
    console.error("Something went wrong:", error.message);
  } finally {
    console.log("Done — whether it worked or not");
  }
}

getUserData(1);

See how much more readable that is? It reads almost like regular synchronous code, from top to bottom. No chaining, no nesting, just clean sequential logic.

The await keyword tells JavaScript — "pause here and wait for this Promise to resolve before moving on."

Real World Example — Fetching Data From an API

Let me show you a real example fetching posts from an API, written both ways so you can see the actual difference side by side.

With Promises:

function getPosts() {
  fetch('https://jsonplaceholder.typicode.com/posts')
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      return response.json();
    })
    .then(posts => {
      console.log('Total posts:', posts.length);
      console.log('First post title:', posts[0].title);
    })
    .catch(error => {
      console.error('Failed to fetch posts:', error.message);
    });
}

getPosts();

With Async/Await:

async function getPosts() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');

    if (!response.ok) {
      throw new Error('Network response was not ok');
    }

    const posts = await response.json();
    console.log('Total posts:', posts.length);
    console.log('First post title:', posts[0].title);

  } catch (error) {
    console.error('Failed to fetch posts:', error.message);
  }
}

getPosts();

Same result, but the async/await version is far easier to follow — especially for developers who are newer to the codebase.

When Promises Are Actually Better

Here is something most tutorials skip — there are real situations where Promises are the better choice over async/await.

Running Multiple Tasks at the Same Time

If you have multiple independent async operations that do not depend on each other, you should run them in parallel instead of one after another.

Wrong way with async/await (slow):

async function loadDashboard() {
  const user    = await getUser();     // waits 1 second
  const posts   = await getPosts();    // then waits 1 more second
  const weather = await getWeather();  // then waits 1 more second
  // Total time: 3 seconds ❌
}

Right way with Promise.all (fast):

async function loadDashboard() {
  const [user, posts, weather] = await Promise.all([
    getUser(),
    getPosts(),
    getWeather()
  ]);
  // Total time: ~1 second ✅ (all run at the same time)
}

Promise.all() runs everything simultaneously and waits for all of them to finish. This can make a massive difference in real applications — especially dashboards that load multiple data sources.

Other Useful Promise Methods

// Promise.race — returns whichever resolves first
const fastest = await Promise.race([apiCall1(), apiCall2()]);

// Promise.allSettled — waits for all, even if some fail
const results = await Promise.allSettled([apiCall1(), apiCall2()]);

// Promise.any — returns first successful one, ignores rejections
const first = await Promise.any([apiCall1(), apiCall2()]);

These methods have no direct async/await equivalent — you need Promises for these patterns. This is exactly why learning both is important.

Common Mistakes to Avoid

1. Forgetting the await Keyword

async function getUser() {
  // ❌ Wrong — returns a Promise object, not actual data
  const user = fetchUser();
  console.log(user); // prints: Promise { <pending> }

  // ✅ Correct
  const user = await fetchUser();
  console.log(user); // prints: { id: 1, name: "Randhir" }
}

2. Not Handling Errors With try/catch

// ❌ Wrong — unhandled rejection, can crash your app
async function getUser() {
  const user = await fetchUser();
}

// ✅ Always wrap await calls in try/catch
async function getUser() {
  try {
    const user = await fetchUser();
    console.log(user);
  } catch (error) {
    console.error("Failed:", error.message);
  }
}

3. Using await Inside a Regular Function

// ❌ This throws a SyntaxError
function getUser() {
  const user = await fetchUser(); // SyntaxError!
}

// ✅ The function MUST be declared async
async function getUser() {
  const user = await fetchUser();
}

Promises vs Async/Await — Side by Side

Promises Async/Await
Syntax .then().catch() chaining Reads like sync code
Error Handling .catch() try/catch
Parallel Execution Promise.all() Promise.all() with await
Debugging Harder to trace Much easier
Readability Gets messy when chained Clean and linear
Browser Support ES6+ ES2017+
Under the Hood Promises Still Promises

So Which One Should You Actually Use?

Honest answer — use async/await by default for most situations. It is cleaner, easier to read, easier to debug, and your teammates will thank you for it.

But learn Promises too, because:

  • Async/await is built on Promises — understanding them makes you a better developer
  • Promise.all(), Promise.race(), and Promise.allSettled() are essential for parallel operations
  • You will encounter Promise-based code in libraries and older codebases regularly
  • Some advanced patterns simply cannot be done with async/await alone

The best developers use both — async/await for clean sequential logic, and Promise methods when running multiple tasks in parallel.

Final Thought

When I first learned this, I made the mistake of thinking I had to choose one and ignore the other. The truth is they work together. Once you are comfortable with both, you will naturally know which one fits each situation without even thinking about it.

Start by converting any .then() chains you already have into async/await. You will immediately see how much cleaner your code becomes — and you will never want to go back to callback hell again.

Got questions about a specific use case? Drop them in the comments or reach out via our contact page — happy to help.