Exercise

Promise chaining is a technique where you link multiple .then() calls one after another. Each .then() returns a new Promise, allowing you to perform asynchronous operations in sequence. The value returned from one .then() callback is passed as the argument to the next .then() in the chain. This avoids deeply nested callbacks and keeps asynchronous code readable and maintainable.

javascript

function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: "Alice" }), 500);
  });
}

function fetchPosts(user) {
  return new Promise((resolve) => {
    setTimeout(() => resolve([`${user.name}'s Post 1`, `${user.name}'s Post 2`]), 500);
  });
}

fetchUser(1)
  .then((user) => {
    console.log("User:", user.name); // "Alice"
    return fetchPosts(user);
  })
  .then((posts) => {
    console.log("Posts:", posts); // ["Alice's Post 1", "Alice's Post 2"]
  });

When a Promise in a chain rejects or a .then() callback throws an error, the rejection propagates down the chain, skipping all subsequent .then() handlers until it reaches a .catch() block. This is similar to how try...catch works in synchronous code. A single .catch() at the end of the chain can handle errors from any step above it.

javascript

function getOrder(orderId) {
  return new Promise((resolve, reject) => {
    if (orderId <= 0) {
      reject(new Error("Invalid order ID"));
    }
    resolve({ orderId, item: "Laptop" });
  });
}

getOrder(-1)
  .then((order) => {
    console.log("Order:", order.item); // skipped
    return order.item;
  })
  .then((item) => {
    console.log("Processing:", item); // skipped
  })
  .catch((error) => {
    console.error("Error caught:", error.message); // "Invalid order ID"
  });

Promise.all() takes an array of Promises and returns a single Promise that resolves when all of the input Promises have resolved. The resolved value is an array of results in the same order as the input Promises. It is useful when you need to run multiple independent asynchronous tasks in parallel and wait for all of them to complete before continuing.

javascript

const fetchName = new Promise((resolve) => {
  setTimeout(() => resolve("Alice"), 300);
});

const fetchAge = new Promise((resolve) => {
  setTimeout(() => resolve(28), 500);
});

const fetchCity = new Promise((resolve) => {
  setTimeout(() => resolve("New York"), 200);
});

Promise.all([fetchName, fetchAge, fetchCity])
  .then(([name, age, city]) => {
    console.log(name); // "Alice"
    console.log(age);  // 28
    console.log(city); // "New York"
  });

If any single Promise passed to Promise.all() rejects, the entire Promise.all() immediately rejects with the reason from the first rejection. The results of all other Promises are discarded, even if some of them resolved successfully. This is called a fail-fast behavior. If you need the results of all Promises regardless of individual failures, consider using Promise.allSettled() instead.

javascript

const task1 = Promise.resolve("Task 1 done");
const task2 = Promise.reject(new Error("Task 2 failed"));
const task3 = Promise.resolve("Task 3 done");

Promise.all([task1, task2, task3])
  .then((results) => {
    console.log(results); // never reached
  })
  .catch((error) => {
    console.error(error.message); // "Task 2 failed"
  });

// Using Promise.allSettled() to get all results
Promise.allSettled([task1, task2, task3])
  .then((results) => {
    console.log(results);
    // [
    //   { status: "fulfilled", value: "Task 1 done" },
    //   { status: "rejected", reason: Error("Task 2 failed") },
    //   { status: "fulfilled", value: "Task 3 done" }
    // ]
  });

Promise.race() accepts an array of Promises and returns a new Promise that settles as soon as the first Promise in the array settles - whether it resolves or rejects. The remaining Promises continue to run but their results are ignored. This is useful for scenarios like implementing timeouts or taking the fastest response from multiple sources.

javascript

const slow = new Promise((resolve) => {
  setTimeout(() => resolve("Slow response"), 3000);
});

const fast = new Promise((resolve) => {
  setTimeout(() => resolve("Fast response"), 500);
});

const medium = new Promise((resolve) => {
  setTimeout(() => resolve("Medium response"), 1500);
});

Promise.race([slow, fast, medium])
  .then((winner) => {
    console.log(winner); // "Fast response"
  });

To convert a callback-based function into one that returns a Promise, you wrap the original function call inside a new Promise() constructor. Inside the executor, you call the original function and use resolve() in the success callback and reject() in the error callback. This process is sometimes called promisification and is a common pattern when working with older APIs that use callbacks.

javascript

// Original callback-based function
function loadData(callback) {
  setTimeout(() => {
    const success = true;
    if (success) {
      callback(null, { id: 1, name: "Product A" });
    } else {
      callback(new Error("Failed to load data"));
    }
  }, 1000);
}

// Promisified version
function loadDataAsync() {
  return new Promise((resolve, reject) => {
    loadData((error, data) => {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

loadDataAsync()
  .then((data) => console.log("Data:", data.name)) // "Product A"
  .catch((error) => console.error(error.message));

When you return a plain value from a .then() callback, it is automatically wrapped in a resolved Promise and passed to the next .then(). When you return a Promise, the chain waits for that Promise to settle before continuing. The next .then() receives the resolved value of the returned Promise. This distinction is important because returning a Promise allows you to perform additional asynchronous work within the chain.

javascript

// Returning a plain value
Promise.resolve(5)
  .then((num) => {
    return num * 2; // returns 10, wrapped in Promise.resolve(10)
  })
  .then((result) => {
    console.log(result); // 10
  });

// Returning a Promise
Promise.resolve(5)
  .then((num) => {
    return new Promise((resolve) => {
      setTimeout(() => resolve(num * 3), 1000);
    });
  })
  .then((result) => {
    console.log(result); // 15 (after 1 second)
  });

To run Promises sequentially, you can chain them using .then() or use Array.reduce() to build a chain dynamically. Unlike Promise.all(), which starts all Promises at once, sequential execution ensures each task completes before the next one begins. This is useful when each step depends on the result of the previous one or when you need to control the order of side effects.

javascript

function delay(value, ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Processed:", value);
      resolve(value);
    }, ms);
  });
}

const items = ["A", "B", "C"];

// Using reduce to run sequentially
items.reduce((chain, item) => {
  return chain.then(() => delay(item, 500));
}, Promise.resolve())
  .then(() => {
    console.log("All items processed in order");
  });

// Output (each 500ms apart):
// "Processed: A"
// "Processed: B"
// "Processed: C"
// "All items processed in order"

Promise.any() takes an array of Promises and resolves as soon as the first Promise fulfills (resolves successfully). It ignores rejections unless all Promises reject, in which case it throws an AggregateError. In contrast, Promise.race() settles with the first Promise to settle - whether it resolves or rejects. Use Promise.any() when you only care about the first successful result and want to ignore failures.

javascript

const p1 = new Promise((_, reject) => {
  setTimeout(() => reject(new Error("p1 failed")), 100);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve("p2 succeeded"), 300);
});

const p3 = new Promise((resolve) => {
  setTimeout(() => resolve("p3 succeeded"), 500);
});

// Promise.any - ignores the rejection of p1
Promise.any([p1, p2, p3])
  .then((result) => {
    console.log(result); // "p2 succeeded"
  });

// Promise.race - settles with p1 because it finishes first
Promise.race([p1, p2, p3])
  .catch((error) => {
    console.error(error.message); // "p1 failed"
  });

You can add a timeout to a Promise by using Promise.race() to race the original Promise against a timer Promise that rejects after a specified duration. If the original Promise does not settle before the timer, the timeout rejection wins the race and the operation is considered timed out. This pattern is commonly used for network requests and other operations where you need to set an upper bound on how long to wait.

javascript

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`Operation timed out after ${ms}ms`));
    }, ms);
  });
  return Promise.race([promise, timeout]);
}

// Simulating a slow API call
const slowRequest = new Promise((resolve) => {
  setTimeout(() => resolve("Data received"), 5000);
});

withTimeout(slowRequest, 2000)
  .then((data) => {
    console.log(data); // not reached in this case
  })
  .catch((error) => {
    console.error(error.message); // "Operation timed out after 2000ms"
  });