What you'll be able to do

By the end of this page you should be able to create a simple Promise, read its three states, attach .then and .catch, and spot an unhandled rejection before it becomes a silent failure in the console.

Previous post: callbacks and timing.

Who this is for

  • People who saw new Promise or .then in a tutorial and froze
  • Beginners escaping nested setTimeout callbacks
  • Anyone who got “Uncaught (in promise)” and did not know where to look

You can skip this if you already use promises daily. Come back when chaining gets messy — that is the next post.

A promise is a receipt for future work

A Promise is an object that represents a value that may arrive later — success or failure. You do not hold the final answer yet; you hold a ticket that says “I’ll tell you when it’s ready.”

Scenario A — ordering food. You get a receipt (promise). Later it becomes “ready” (fulfilled) or “sold out” (rejected).

Scenario B — loading a file. The browser starts the request; your code keeps running; when the result arrives, your .then runs.

Three states

// pending → fulfilled with a value
// pending → rejected with a reason (often an Error)

Scenario A — happy path. Promise fulfills with "ok".

Scenario B — failure path. Promise rejects with new Error("nope").

A promise settles once. It does not flip from fulfilled back to pending.

Creating one (for learning)

const later = new Promise(function (resolve, reject) {
  setTimeout(function () {
    resolve("done");
    // reject(new Error("failed")); // try this path too
  }, 500);
});

later.then(function (value) {
  console.log(value); // "done"
});

Scenario A — wrap a timer. Teaching tool; real APIs often return promises for you (fetch next).

Scenario B — reject path. Call reject(err) instead; handle with .catch.

You rarely write new Promise for everyday app code once libraries return promises — but understanding resolve / reject removes the fog.

then and catch

Promise.resolve(2)
  .then(function (n) {
    return n * 3;
  })
  .then(function (n) {
    console.log(n); // 6
  })
  .catch(function (err) {
    console.error(err);
  });

Scenario A — transform the value. Return from .then passes to the next .then.

Scenario B — error. Throw or reject jumps to the nearest .catch.

Returning a plain value from .then is fine. Returning another promise waits for that one — chaining deepens in the next blog.

Unhandled rejection

Promise.reject(new Error("oops"));
// Console: Uncaught (in promise) — nothing caught it

Scenario A — forgot .catch. Always attach one (or use try/catch with async/await later).

Scenario B — fire-and-forget. Even “background” work should log failures.

Promise.resolve / Promise.reject (shortcuts)

Promise.resolve(42).then(console.log);
Promise.reject(new Error("x")).catch(console.error);

Useful in demos and tests when you already have a value or error and want promise-shaped code.

A tiny practice file

Save as promise-basics.html.

<!DOCTYPE html>
<html>
  <body>
    <button id="ok">Resolve</button>
    <button id="bad">Reject</button>
    <pre id="out"></pre>
    <script>
      const out = document.getElementById("out");
      function wait(ms, ok) {
        return new Promise(function (resolve, reject) {
          setTimeout(function () {
            if (ok) resolve("success after " + ms + "ms");
            else reject(new Error("failed after " + ms + "ms"));
          }, ms);
        });
      }
      document.getElementById("ok").addEventListener("click", function () {
        out.textContent = "pending…";
        wait(600, true)
          .then(function (msg) {
            out.textContent = msg;
          })
          .catch(function (err) {
            out.textContent = String(err);
          });
      });
      document.getElementById("bad").addEventListener("click", function () {
        out.textContent = "pending…";
        wait(600, false)
          .then(function (msg) {
            out.textContent = msg;
          })
          .catch(function (err) {
            out.textContent = err.message;
          });
      });
    </script>
  </body>
</html>

Mistakes I see a lot

1. Ignoring rejections. Always .catch or equivalent.

2. Nesting .then inside .then when a flat chain would do. Next post fixes the habit.

3. Treating promises like sync values. const x = fetch(...) is a Promise, not JSON — wait with .then / await.

4. Calling resolve twice. Second call is ignored; confusing when debugging.

5. Throwing outside the promise executor and expecting .catch to see it. Sync throws before the promise exists need normal try/catch.

What to try before the next post

  1. Build a resolve and a reject path with buttons.
  2. Leave off .catch once; read the console warning; then add it.
  3. Chain two .then calls that multiply a number.
  4. Build promise-basics.html.

Next in this series: promise chaining — flat chains vs nested callbacks leftovers.

Try this next outside the series

Async code gets much easier when you see the same ideas on the browser side and the server side.