03

Promises: a getter for one value from the past or the future

A promise is the temporal analogue of a value. Its setter is the resolver, its getter is the promise, and together they form a deferred. Hand the resolver to any number of producers and the promise to any number of consumers: nobody can tell whether they were first, last, early or late.
Promises are broadcast. … One consumer cannot prevent another consumer from making progress. Information flows in one direction.

One deferred: a resolver and a promise

setter producers hold the resolver
producer A
resolver.return("🍎 apple")
producer B
resolver.return("🍌 banana")
producer C
resolver.return("🍒 cherry")
producer D
resolver.return(sleep(1500) → "🫐 blueberry")
deferred
Promisepending

Only the first call to the resolver counts. Every later call is silently ignored — the losers can't tell they lost.

getter consumers hold the promise

Add observers before or after resolution. Every one of them sees the same value.

then: the map and flatMap of a singular value

defer()
promiseForTen
pending
→
.then(ten => ten + 20)
promiseForThirty
pending
→
.then(n => n * 2)
promiseForSixty
pending
⤳
.catch(e => 0)
recovered
pending

When an observer returns a promise, the next link is resolved with that promise and waits for it — a deferred value deferred further. When an observer throws, the error skips every then until a catch recovers.

then, sketched: create a deferred, forward the observer's result

Promise.prototype.then = function (onreturn, onthrow) {
  var deferred = Promise.defer();
  var resolver = deferred.resolver;
  this.done(function (value) {
    if (onreturn) {
      try { resolver.return(onreturn(value)); }
      catch (error) { resolver.throw(error); }
    } else {
      resolver.return(value);
    }
  }, function (error) {
    if (onthrow) {
      try { resolver.return(onthrow(error)); }
      catch (error) { resolver.throw(error); }
    } else {
      resolver.throw(error);
    }
  });
  return deferred.promise;
};

Resolver vocabulary: return is resolve, throw is reject

// gtor's names                 // the standard's names
resolver.return(10);            resolve(10);
resolver.return(promise);       resolve(promise);
resolver.throw(error);          reject(error);

// The standard hides the deferred behind a "revealing constructor":
var promise = new Promise(function (resolve, reject) {
  // ...only the code in here holds the setter
});
  • why it mattersSubscribing before or after resolution makes no difference. A promise models dependency: the value is there whenever you ask for it, and every observer sees the same one.
  • why it mattersRacing producers have indistinguishable experiences — every return() returns undefined. That makes resolvers safe to hand out widely.
  • why it mattersThe price of broadcast: a promise represents a result, not the work leading to it. No consumer may abort that work, because others may depend on it. For that you need a task.