Episode 1 — Fundamentals / 1.17 — Asynchronous Programming JavaScript

1.17.c — Understanding Promises

In one sentence: A Promise is an object representing a future value — it starts pending, then settles to fulfilled (with a value) or rejected (with a reason) — and provides .then(), .catch(), .finally() for composable, chainable async control flow.

Navigation: ← 1.17.b — Callbacks · 1.17.d — Async/Await →


1. What is a Promise?

A Promise is like an IOU — a placeholder for a value that does not exist yet but will (or will fail) in the future.

// Real-world analogy:
// You order coffee. The cashier gives you a receipt (Promise).
// The receipt is "pending" while the barista makes it.
// When ready, the receipt "fulfills" — you get your coffee (value).
// If they run out of beans, the receipt "rejects" — you get an apology (error).

Unlike callbacks, you hold the Promise object and decide when and how to react — no inversion of control.


2. Promise states

A Promise is always in one of three states:

              ┌──── fulfilled (resolved) ── carries a value
              │
  pending ────┤
              │
              └──── rejected ── carries a reason (error)
StateMeaningSettled?
pendingOperation in progress, no result yetNo
fulfilledOperation succeeded, value is availableYes
rejectedOperation failed, reason (error) is availableYes

Key rules:

  • A Promise settles once. It cannot go from fulfilled to rejected or vice versa.
  • Once settled, the value or reason is immutable.

3. Creating a Promise

Use the Promise constructor with an executor function that receives resolve and reject:

const myPromise = new Promise(function (resolve, reject) {
  // Async operation here
  const success = true;

  if (success) {
    resolve("Operation completed!");    // → fulfilled with this value
  } else {
    reject(new Error("Something failed")); // → rejected with this reason
  }
});

Real example: wrapping setTimeout in a Promise

function delay(ms) {
  return new Promise(function (resolve) {
    setTimeout(resolve, ms);
  });
}

delay(2000).then(function () {
  console.log("2 seconds have passed");
});

Real example: simulating a network request

function fetchUser(userId) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      if (userId <= 0) {
        reject(new Error("Invalid user ID"));
        return;
      }
      resolve({ id: userId, name: "Alice", email: "alice@example.com" });
    }, 1000);
  });
}

4. Consuming Promises: .then(), .catch(), .finally()

4.1 .then(onFulfilled, onRejected)

fetchUser(1)
  .then(function (user) {
    console.log("User:", user.name);  // runs if fulfilled
  });

.then() can also take a second argument for rejection (rarely used in practice):

fetchUser(-1)
  .then(
    function (user) { console.log(user); },
    function (err)  { console.error("Rejected:", err.message); }
  );

4.2 .catch(onRejected)

Preferred way to handle errors — equivalent to .then(null, onRejected):

fetchUser(-1)
  .then(function (user) {
    console.log(user);
  })
  .catch(function (err) {
    console.error("Error:", err.message);  // "Error: Invalid user ID"
  });

4.3 .finally(onFinally)

Runs regardless of fulfillment or rejection — useful for cleanup:

showLoadingSpinner();

fetchUser(1)
  .then(function (user) {
    renderUser(user);
  })
  .catch(function (err) {
    showError(err.message);
  })
  .finally(function () {
    hideLoadingSpinner();  // always runs
  });

5. Promise chaining

Each .then() returns a new Promise, allowing you to chain operations sequentially:

fetchUser(1)
  .then(function (user) {
    console.log("Got user:", user.name);
    return fetchPosts(user.id);           // return a Promise → next .then waits for it
  })
  .then(function (posts) {
    console.log("Got posts:", posts.length);
    return fetchComments(posts[0].id);    // return another Promise
  })
  .then(function (comments) {
    console.log("Got comments:", comments.length);
  })
  .catch(function (err) {
    console.error("Something failed:", err.message); // catches ANY rejection above
  });

Compare to callback hell — the same logic is flat, readable, and has centralized error handling.

How chaining works

What .then() callback returnsWhat the next .then() receives
A value (number, string, object)That value directly
A PromiseThe resolved value of that Promise
Throws an errorNext .catch() receives the error
Nothing (undefined)undefined
Promise.resolve(10)
  .then(function (x) { return x * 2; })      // returns 20
  .then(function (x) { return x + 5; })      // returns 25
  .then(function (x) { console.log(x); });   // logs 25

6. Promise static methods

6.1 Promise.all(iterable)

Waits for all promises to fulfill. Rejects immediately if any one rejects (fail-fast).

const p1 = fetch("/api/users");
const p2 = fetch("/api/posts");
const p3 = fetch("/api/comments");

Promise.all([p1, p2, p3])
  .then(function (responses) {
    // responses is an array of all three Response objects
    console.log("All fetched:", responses.length); // 3
  })
  .catch(function (err) {
    console.error("At least one failed:", err);
  });

6.2 Promise.allSettled(iterable)

Waits for all promises to settle (fulfill or reject). Never short-circuits.

const promises = [
  fetch("/api/users"),
  fetch("/api/broken-endpoint"),  // might reject
  fetch("/api/posts"),
];

Promise.allSettled(promises).then(function (results) {
  results.forEach(function (result, i) {
    if (result.status === "fulfilled") {
      console.log("Promise", i, "succeeded:", result.value);
    } else {
      console.log("Promise", i, "failed:", result.reason);
    }
  });
});

6.3 Promise.race(iterable)

Settles with the first promise to settle (fulfill or reject).

function timeout(ms) {
  return new Promise(function (_, reject) {
    setTimeout(function () { reject(new Error("Timeout!")); }, ms);
  });
}

Promise.race([
  fetch("/api/data"),
  timeout(5000)
])
  .then(function (response) { console.log("Got data in time"); })
  .catch(function (err)     { console.error(err.message); }); // "Timeout!" if > 5s

6.4 Promise.any(iterable)

Resolves with the first fulfilled promise. Rejects only if all reject (with AggregateError).

Promise.any([
  fetch("https://cdn1.example.com/data.json"),
  fetch("https://cdn2.example.com/data.json"),
  fetch("https://cdn3.example.com/data.json"),
])
  .then(function (fastest) {
    console.log("First successful response:", fastest);
  })
  .catch(function (err) {
    console.error("All CDNs failed:", err.errors); // AggregateError
  });

Comparison table

MethodResolves whenRejects whenUse case
Promise.allAll fulfillAny rejects (fail-fast)Parallel tasks that all must succeed
Promise.allSettledAll settleNever (always resolves)Need results of all, regardless of failure
Promise.raceFirst settlesFirst settles (if rejection)Timeout patterns, fastest response
Promise.anyFirst fulfillsAll rejectRedundant sources, first success wins

7. Error propagation in promise chains

Errors propagate down the chain until a .catch() handles them:

fetchUser(1)
  .then(function (user) {
    throw new Error("Oops!");  // error thrown here
    return fetchPosts(user.id);
  })
  .then(function (posts) {
    console.log(posts);        // SKIPPED — error is propagating
  })
  .then(function (comments) {
    console.log(comments);     // SKIPPED — error is propagating
  })
  .catch(function (err) {
    console.error("Caught:", err.message); // "Caught: Oops!" — caught here
  })
  .then(function () {
    console.log("This runs!");  // chain continues after .catch()
  });

Key insight: .catch() itself returns a Promise. If the catch handler does not throw, the chain recovers and subsequent .then() calls run normally.


8. Converting callback-based code to Promises

Wrap the callback API in a new Promise:

// Before: callback-based
function readFileCallback(path, callback) {
  fs.readFile(path, "utf8", callback);
}

// After: Promise-based
function readFilePromise(path) {
  return new Promise(function (resolve, reject) {
    fs.readFile(path, "utf8", function (err, data) {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

// Usage
readFilePromise("config.json")
  .then(function (data) { console.log(data); })
  .catch(function (err) { console.error(err); });

Node.js provides a built-in utility for this pattern:

const { promisify } = require("util");
const readFile = promisify(fs.readFile);

readFile("config.json", "utf8")
  .then(data => console.log(data));

9. Common mistakes with Promises

Mistake 1: Forgetting to return in .then()

// WRONG — fetchPosts result is lost
fetchUser(1).then(function (user) {
  fetchPosts(user.id);                    // no return!
}).then(function (posts) {
  console.log(posts);                      // undefined — not the posts!
});

// CORRECT
fetchUser(1).then(function (user) {
  return fetchPosts(user.id);             // return the Promise
}).then(function (posts) {
  console.log(posts);                      // actual posts data
});

Mistake 2: Nesting .then() instead of chaining

// WRONG — recreating callback hell with Promises
fetchUser(1).then(function (user) {
  fetchPosts(user.id).then(function (posts) {
    fetchComments(posts[0].id).then(function (comments) {
      console.log(comments);               // nested mess
    });
  });
});

// CORRECT — flat chain
fetchUser(1)
  .then(function (user) { return fetchPosts(user.id); })
  .then(function (posts) { return fetchComments(posts[0].id); })
  .then(function (comments) { console.log(comments); })
  .catch(function (err) { console.error(err); });

Mistake 3: Missing .catch() at the end

// WRONG — unhandled rejection (may crash Node.js or show console warning)
fetchUser(-1).then(function (user) {
  console.log(user);
});

// CORRECT
fetchUser(-1)
  .then(function (user) { console.log(user); })
  .catch(function (err) { console.error("Handled:", err.message); });

10. Quick-reference: Promise.resolve() and Promise.reject()

// Create an already-fulfilled Promise
const resolved = Promise.resolve(42);
resolved.then(function (val) { console.log(val); }); // 42

// Create an already-rejected Promise
const rejected = Promise.reject(new Error("fail"));
rejected.catch(function (err) { console.error(err.message); }); // "fail"

// Useful for starting a chain or testing
Promise.resolve()
  .then(function () { return fetchUser(1); })
  .then(function (user) { console.log(user); });

Key takeaways

  1. A Promise represents a future value — pending, fulfilled, or rejected.
  2. Use .then() for success, .catch() for errors, .finally() for cleanup.
  3. Chaining keeps async code flat — each .then() returns a new Promise.
  4. Promise.all = all must succeed; Promise.allSettled = get all results; Promise.race = first to settle; Promise.any = first to succeed.
  5. Errors propagate through the chain until caught by .catch().
  6. Always return inside .then() and always add .catch() at the end of a chain.

Explain-It Challenge

Explain without notes:

  1. What are the three states of a Promise, and can a Promise change state more than once?
  2. What is the difference between Promise.all and Promise.allSettled?
  3. Why is nesting .then() inside another .then() considered an anti-pattern?

Navigation: ← 1.17.b — Callbacks · 1.17.d — Async/Await →