Promises

Advanced · 18 min read · ▶ live playground · ✦ checkpoint

A promise lets JavaScript hand you a receipt before the work is done. A Promise is an object that represents one future result: not many results, not a stream, exactly one success or one failure. That one object is what lets async code flatten out instead of growing into the callback pyramid.

One Future Result, Three States

A state is the promise's current label. A promise starts pending, which means the future result has not arrived yet. It becomes fulfilled when the work succeeds with a value, or rejected when the work fails with a reason, usually an Error.

Once a promise is fulfilled or rejected, it is settled, which means its final state is locked. Like a pickup receipt, it starts as "not ready yet," then ends as either "ready" or "couldn't be made."

function loadScore(player, delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ player, score: 44 });
    }, delay);
  });
}
 
const scorePromise = loadScore("Mina", 10);
 
console.log("promise created");
 
scorePromise.then(record => {
  console.log(`${record.player}: ${record.score}`);
});
 
console.log("script finished");

promise created, script finished, then Mina: 44.

loadScore returns immediately with a pending promise. Later, the timer calls resolve, which fulfills the promise with the score record. The .then(...) callback waits for that fulfillment.

then, catch, and finally

A handler is a callback attached to a promise. .then(handler) runs when the promise is fulfilled. .catch(handler) runs when the promise is rejected. .finally(handler) runs after settlement either way, and it receives no result because it is for cleanup.

function reserveSeat(player, seatAvailable, delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!seatAvailable) {
        reject(new Error(`${player} has no seat`));
        return;
      }
 
      resolve(`${player} reserved seat A3`);
    }, delay);
  });
}
 
reserveSeat("Rae", true, 10)
  .then(message => {
    console.log(message);
  })
  .catch(error => {
    console.log(`problem: ${error.message}`);
  })
  .finally(() => {
    console.log("Rae check finished");
  });
 
reserveSeat("Kai", false, 20)
  .then(message => {
    console.log(message);
  })
  .catch(error => {
    console.log(`problem: ${error.message}`);
  })
  .finally(() => {
    console.log("Kai check finished");
  });

Rae reserved seat A3, Rae check finished, problem: Kai has no seat, then Kai check finished.

The error doesn't go through .then. It jumps to .catch, just like a thrown error jumps to catch in synchronous code. Promise callbacks run as microtasks, so they run after the current script finishes but before the next task.

Try it — watch settlement

Run this once, then read the order. The last line is minaPrize, so the playground also shows the promise itself: first as Promise { <pending> }, then with its settled value.

javascript — live consolereal engine
⌘/Ctrl + Enter to run

At the prompt, try nicoPrize. You will see that the same promise object now remembers its rejection, even though the .catch already handled it.

Chain Flat, Not Nested

A chain is the line of promises created when each .then returns a value or another promise. If a .then returns a plain value, the next .then receives that value. If it returns a promise, the next .then waits for that promise to settle.

function choosePlayer() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Mina");
    }, 10);
  });
}
 
function loadScore(player) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ player, score: 42 });
    }, 20);
  });
}
 
function saveBadge(record) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`${record.player} earned the Gold badge.`);
    }, 30);
  });
}
 
choosePlayer()
  .then(player => {
    return loadScore(player);
  })
  .then(record => {
    return saveBadge(record);
  })
  .then(message => {
    console.log(message);
  })
  .catch(error => {
    console.log(`chain failed: ${error.message}`);
  });

Mina earned the Gold badge.

Each step hands the next async receipt forward. The code stays flat because the next .then belongs to the chain, not inside the previous callback.

The quiet bug is forgetting return. Watch this closely:

function choosePlayer() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Mina");
    }, 10);
  });
}
 
function loadScore(player) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`loaded score for ${player}`);
      resolve({ player, score: 42 });
    }, 20);
  });
}
 
choosePlayer()
  .then(player => {
    loadScore(player);
  })
  .then(record => {
    console.log(`next step received: ${record}`);
  });

next step received: undefined, then loaded score for Mina.

The first .then starts loadScore, but it does not return that promise. A block-body arrow with no return returns undefined, so the next .then receives undefined right away. The fix is one word: return loadScore(player);.

Create Promises and Wrap Old Callbacks

The function you pass to new Promise(...) is the executor, a function that starts the async work immediately. It receives resolve, which fulfills the promise, and reject, which rejects it.

Promisifying means wrapping an old callback API so it returns a promise instead. You keep the old function at the edge, then write modern promise code everywhere else.

function readSavedScore(player, delay, done) {
  setTimeout(() => {
    if (player === "") {
      done(new Error("Player name is required"));
      return;
    }
 
    done(null, { player, score: 31 });
  }, delay);
}
 
function readSavedScorePromise(player, delay) {
  return new Promise((resolve, reject) => {
    readSavedScore(player, delay, (error, record) => {
      if (error) {
        reject(error);
        return;
      }
 
      resolve(record);
    });
  });
}
 
readSavedScorePromise("Rae", 10)
  .then(record => {
    console.log(`${record.player}: ${record.score}`);
  });
 
readSavedScorePromise("", 20)
  .catch(error => {
    console.log(`failed: ${error.message}`);
  });

Rae: 31, then failed: Player name is required.

Promises don't erase callbacks. They organize them. A promise API gives you one settled result, one .catch path, and a flat chain that can keep growing without turning sideways.

Next, async and await make this same promise machine read like straight-line code. The machine does not change; the syntax gets kinder.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion