Promises Deep Dive

Promise internals, chaining, error handling, Promise.all, Promise.race, and more

Understanding Promises

A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises provide a cleaner alternative to callbacks for handling async code.

Promise States

  • Pending — Initial state, neither fulfilled nor rejected
  • Fulfilled — Operation completed successfully
  • Rejected — Operation failed
  • Settled — Either fulfilled or rejected (final state)

Creating Promises

// Basic Promise creation
const promise = new Promise((resolve, reject) => {
  // Async operation
  setTimeout(() => {
    const success = true;
    
    if (success) {
      resolve("Operation succeeded!"); // Fulfill
    } else {
      reject(new Error("Operation failed")); // Reject
    }
  }, 1000);
});

// Consuming the promise
promise
  .then(result => console.log(result))  // "Operation succeeded!"
  .catch(error => console.error(error));

// Shorthand for resolved/rejected promises
Promise.resolve("immediate value");
Promise.reject(new Error("immediate error"));

Promise Chaining

.then() always returns a new Promise, enabling chaining:

fetch("/api/user")
  .then(response => response.json())   // Returns Promise
  .then(user => fetch(`/api/posts/${user.id}`))  // Returns Promise
  .then(response => response.json())
  .then(posts => {
    console.log("Posts:", posts);
    return posts.length;
  })
  .then(count => console.log(`Found ${count} posts`))
  .catch(error => console.error("Error:", error));

// Each .then() receives the value from the previous one
// Returning a Promise waits for it to resolve

Error Handling

// .catch() handles errors from any point in the chain
fetchUser()
  .then(user => fetchPosts(user.id))
  .then(posts => displayPosts(posts))
  .catch(error => {
    // Catches errors from fetchUser, fetchPosts, or displayPosts
    console.error("Something went wrong:", error);
  });

// Multiple catch blocks for specific handling
fetchUser()
  .then(user => {
    if (!user.isActive) {
      throw new Error("User inactive");
    }
    return fetchPosts(user.id);
  })
  .catch(error => {
    if (error.message === "User inactive") {
      return []; // Return empty array, chain continues
    }
    throw error; // Re-throw other errors
  })
  .then(posts => console.log(posts));

// .finally() runs regardless of success/failure
fetchData()
  .then(data => process(data))
  .catch(error => handleError(error))
  .finally(() => {
    hideLoadingSpinner(); // Always runs
  });

Promise.all()

Wait for multiple promises to resolve. Rejects if any promise rejects:

const promise1 = fetch("/api/users");
const promise2 = fetch("/api/posts");
const promise3 = fetch("/api/comments");

Promise.all([promise1, promise2, promise3])
  .then(([users, posts, comments]) => {
    // All three resolved
    console.log("Users:", users);
    console.log("Posts:", posts);
    console.log("Comments:", comments);
  })
  .catch(error => {
    // First rejection fails the whole thing
    console.error("One request failed:", error);
  });

// Practical example: Parallel API calls
async function loadDashboard() {
  const [user, notifications, stats] = await Promise.all([
    fetchUser(),
    fetchNotifications(),
    fetchStats()
  ]);
  
  return { user, notifications, stats };
}

Promise.allSettled()

Wait for all promises to settle, regardless of success/failure:

const promises = [
  Promise.resolve("Success"),
  Promise.reject("Error"),
  Promise.resolve("Another success")
];

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

// Output:
// Promise 0: Success
// Promise 1 failed: Error
// Promise 2: Another success

Promise.race()

Returns when the first promise settles (resolves or rejects):

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

fetchWithTimeout("/api/slow-endpoint", 3000)
  .then(response => response.json())
  .catch(error => console.error(error)); // "Timeout" if > 3s

Promise.any()

Returns when the first promise fulfills. Only rejects if all reject:

// Try multiple sources, use first success
const mirrors = [
  fetch("https://mirror1.com/data"),
  fetch("https://mirror2.com/data"),
  fetch("https://mirror3.com/data")
];

Promise.any(mirrors)
  .then(response => {
    console.log("Got data from fastest mirror");
    return response.json();
  })
  .catch(error => {
    // AggregateError if ALL mirrors failed
    console.error("All mirrors failed:", error.errors);
  });

Creating Promise Utilities

// Delay function
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

await delay(1000); // Wait 1 second

// Retry with exponential backoff
async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (i === retries - 1) throw error;
      await delay(Math.pow(2, i) * 1000); // 1s, 2s, 4s
    }
  }
}

// Sequential execution
async function sequential(tasks) {
  const results = [];
  for (const task of tasks) {
    results.push(await task());
  }
  return results;
}

Common Patterns

// Promisify callback-based function
function promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) reject(error);
        else resolve(result);
      });
    });
  };
}

const readFileAsync = promisify(fs.readFile);
const data = await readFileAsync("file.txt", "utf8");

// Memoized promise (cache result)
function memoizedFetch(url) {
  const cache = new Map();
  
  return function() {
    if (!cache.has(url)) {
      cache.set(url, fetch(url).then(r => r.json()));
    }
    return cache.get(url);
  };
}

⚠️ Common Mistakes

  • Forgetting to return: Always return promises in .then() chains
  • Nesting instead of chaining: Avoid callback hell in promises
  • Unhandled rejections: Always have a .catch() or try/catch
  • Creating unnecessary promises: Don't wrap fetch() in new Promise()

💡 Key Takeaways

  • • Promises represent eventual values and have three states
  • • Chain with .then(), handle errors with .catch(), cleanup with .finally()
  • • Use Promise.all() for parallel operations, Promise.race() for timeouts
  • • Promise.allSettled() when you need all results regardless of failures
  • • Always handle rejections to avoid unhandled promise rejection warnings