What you'll be able to do

By the end of this page you should be able to write a flat .then chain, return either a value or another promise from each step, and keep one .catch at the end instead of nesting pyramids.

Previous post: promises plainly.

Who this is for

  • People who nested .then inside .then because that is how callbacks felt
  • Beginners who lose track of which value arrives in which step
  • Anyone about to learn async/await — chaining is the bridge

You can skip this if flat chains already feel automatic. Come back when a review comment says “flatten this.”

Nested leftover (hard to read)

wait(200).then(function () {
  return wait(200).then(function () {
    return wait(200).then(function () {
      console.log("done");
    });
  });
});

Scenario A — three delayed steps. Nesting works but grows sideways.

Scenario B — error handling. You need .catch on every level or one outer — easy to miss.

Flat chain (preferred shape)

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

wait(200)
  .then(function () {
    console.log("step 1");
    return wait(200);
  })
  .then(function () {
    console.log("step 2");
    return wait(200);
  })
  .then(function () {
    console.log("step 3");
  })
  .catch(function (err) {
    console.error(err);
  });

Scenario A — sequential UI steps. Load config → then load user → then render.

Scenario B — one failure. Any step that rejects jumps to the final .catch.

Return a value vs return a promise

Promise.resolve(2)
  .then(function (n) {
    return n + 1; // plain value → next then gets 3
  })
  .then(function (n) {
    return wait(100).then(function () {
      return n * 10;
    }); // returning a promise waits for it
  })
  .then(function (n) {
    console.log(n); // 30
  });

Scenario A — sync transform. Return the number.

Scenario B — async step in the middle. Return the inner promise (or later await in async functions).

Rule: whatever you return from .then becomes the input to the next .then (after waiting if it was a promise).

Forgetting return (classic bug)

.wait(100)
  .then(function () {
    wait(100); // missing return — next then runs too early
  })

Scenario A — race. Next step starts before the inner wait finishes.

Scenario B — undefined next value. The next .then receives undefined because nothing was returned.

A tiny practice file

Save as promise-chain.html.

<!DOCTYPE html>
<html>
  <body>
    <button id="go">Run chain</button>
    <pre id="out"></pre>
    <script>
      const out = document.getElementById("out");
      function wait(ms, label) {
        return new Promise(function (resolve) {
          setTimeout(function () {
            out.textContent += label + "\n";
            resolve(label);
          }, ms);
        });
      }
      document.getElementById("go").addEventListener("click", function () {
        out.textContent = "";
        wait(300, "1")
          .then(function () {
            return wait(300, "2");
          })
          .then(function () {
            return wait(300, "3");
          })
          .then(function () {
            out.textContent += "done";
          })
          .catch(function (err) {
            out.textContent += String(err);
          });
      });
    </script>
  </body>
</html>

Mistakes I see a lot

1. Nesting every .then. Prefer flat returns.

2. Missing return before an inner promise.

3. Putting .catch only on the first promise. Attach at the end of the chain.

4. Assuming .then runs sync. Even Promise.resolve().then(...) is async relative to the current stack.

What to try before the next post

  1. Rewrite a nested example as a flat chain.
  2. Break a chain by omitting return; watch the order.
  3. Add a rejecting step and confirm one .catch handles it.
  4. Build promise-chain.html.

Next in this series: async/await everyday — the same chains, easier to read.

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.