Introduction

JavaScript Promises revolutionized asynchronous programming. They replaced callback hell with clean, chainable code. Today, async/await makes promises even more elegant. This guide covers everything you need to know.

The Problem: Callback Hell

Before Promises

// ❌ Callback hell
getData((data) => {
  processData(data, (result) => {
    saveData(result, (saved) => {
      sendNotification(saved, (sent) => {
        console.log("Done!");
        // Deeply nested, hard to read
      });
    });
  });
});

Problems:

  • Hard to read
  • Difficult to debug
  • Error handling is messy
  • No easy way to cancel

What are Promises?

A Promise represents a value that may not be available yet, but will be resolved (or rejected) in the future.

Promise States

Pending → Fulfilled (Resolved)

   Rejected

Three states:

  1. Pending: Initial state, neither fulfilled nor rejected
  2. Fulfilled: Operation completed successfully
  3. Rejected: Operation failed

Creating Promises

// Explicit Promise creation
const promise = new Promise((resolve, reject) => {
  // Async operation
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve("Data loaded");
    } else {
      reject("Error occurred");
    }
  }, 1000);
});

Using Promises

Basic Promise Usage

const promise = fetch("/api/data");

promise
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

Promise Chaining

fetch("/api/users")
  .then((response) => response.json())
  .then((users) => users[0])
  .then((user) => fetch(`/api/users/${user.id}/posts`))
  .then((response) => response.json())
  .then((posts) => console.log(posts))
  .catch((error) => console.error("Error:", error));

Promise Methods

Promise.all()

Wait for all promises to resolve:

const promise1 = fetch("/api/data1");
const promise2 = fetch("/api/data2");
const promise3 = fetch("/api/data3");

Promise.all([promise1, promise2, promise3])
  .then((responses) => {
    // All resolved
    return Promise.all(responses.map((r) => r.json()));
  })
  .then((results) => {
    console.log(results); // [data1, data2, data3]
  })
  .catch((error) => {
    // If ANY promise rejects, this catches it
    console.error(error);
  });

Promise.allSettled()

Wait for all promises to settle (resolve or reject):

const promises = [
  fetch("/api/data1"),
  fetch("/api/data2"), // Might fail
  fetch("/api/data3"),
];

Promise.allSettled(promises).then((results) => {
  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      console.log(`Promise ${index}:`, result.value);
    } else {
      console.error(`Promise ${index}:`, result.reason);
    }
  });
});

Promise.race()

First promise to settle wins:

const fastAPI = fetch("/api/fast");
const slowAPI = fetch("/api/slow");

Promise.race([fastAPI, slowAPI])
  .then((response) => {
    // First response wins
    console.log("Got response:", response);
  })
  .catch((error) => {
    // First error wins
    console.error("Error:", error);
  });

Promise.any()

First promise to fulfill (ignores rejections):

const backup1 = fetch("/api/primary");
const backup2 = fetch("/api/backup1");
const backup3 = fetch("/api/backup2");

Promise.any([backup1, backup2, backup3])
  .then((response) => {
    // First successful response
    console.log("Got data from:", response.url);
  })
  .catch((error) => {
    // All promises rejected
    console.error("All APIs failed");
  });

Async/Await

Basic Syntax

// Before: Promises
fetch("/api/data")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

// After: async/await
async function fetchData() {
  try {
    const response = await fetch("/api/data");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

fetchData();

Multiple Awaits

async function getUserData(userId) {
  const user = await fetch(`/api/users/${userId}`);
  const posts = await fetch(`/api/users/${userId}/posts`);
  const comments = await fetch(`/api/users/${userId}/comments`);

  return {
    user: await user.json(),
    posts: await posts.json(),
    comments: await comments.json(),
  };
}

Parallel Execution

// Sequential (slow)
async function sequential() {
  const data1 = await fetch("/api/data1");
  const data2 = await fetch("/api/data2");
  const data3 = await fetch("/api/data3");
  // Total time: time1 + time2 + time3
}

// Parallel (fast)
async function parallel() {
  const [data1, data2, data3] = await Promise.all([
    fetch("/api/data1"),
    fetch("/api/data2"),
    fetch("/api/data3"),
  ]);
  // Total time: max(time1, time2, time3)
}

Error Handling

Try/Catch with Async/Await

async function fetchData() {
  try {
    const response = await fetch("/api/data");
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Fetch failed:", error);
    // Handle error or rethrow
    throw error;
  }
}

Multiple Error Handling

async function complexOperation() {
  try {
    const user = await getUser();
    try {
      const posts = await getPosts(user.id);
      return { user, posts };
    } catch (postError) {
      // Handle post error specifically
      return { user, posts: [] };
    }
  } catch (userError) {
    // Handle user error
    throw new Error("Failed to load user");
  }
}

Common Patterns

Retry Logic

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (response.ok) return response;
      throw new Error("Request failed");
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

Timeout

function fetchWithTimeout(url, timeout = 5000) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("Timeout")), timeout)
    ),
  ]);
}

Sequential Processing

async function processItems(items) {
  const results = [];
  for (const item of items) {
    const result = await processItem(item);
    results.push(result);
  }
  return results;
}

Best Practices

✅ Do This

1. Always handle errors

async function safeOperation() {
  try {
    return await riskyOperation();
  } catch (error) {
    console.error("Operation failed:", error);
    return defaultValue;
  }
}

2. Use Promise.all for parallel operations

const [user, posts, comments] = await Promise.all([
  fetchUser(),
  fetchPosts(),
  fetchComments(),
]);

3. Return promises from async functions

async function getData() {
  const response = await fetch("/api/data");
  return response.json(); // Returns a Promise
}

❌ Don’t Do This

1. Don’t forget await

// ❌ Missing await
async function fetchData() {
  const data = fetch("/api/data"); // Returns Promise, not data!
  return data;
}

// ✅ Correct
async function fetchData() {
  const response = await fetch("/api/data");
  return response.json();
}

2. Don’t mix promise chains with async/await unnecessarily

// ❌ Unnecessary mixing
async function fetchData() {
  return fetch("/api/data")
    .then((response) => response.json())
    .then((data) => data.items);
}

// ✅ Cleaner with async/await
async function fetchData() {
  const response = await fetch("/api/data");
  const data = await response.json();
  return data.items;
}

3. Don’t create promise chains in loops

// ❌ Creates promises sequentially
for (const url of urls) {
  await fetch(url); // Waits for each
}

// ✅ Parallel execution
await Promise.all(urls.map((url) => fetch(url)));

Visualizing Promises

Use our tools:

Conclusion

Promises and async/await make JavaScript asynchronous code:

Key benefits:

  • Clean, readable code
  • Better error handling
  • Easy to chain operations
  • Parallel execution support

Remember:

  • Always handle errors
  • Use async/await for readability
  • Use Promise.all for parallel operations
  • Don’t forget await!

Next Steps