Skip to content

Latest commit

 

History

History
147 lines (125 loc) · 5.22 KB

async-values.md

File metadata and controls

147 lines (125 loc) · 5.22 KB

Asynchronous Values

The asynchronous analogue of a getter is a promise. Each promise has a corresponding resolver as its asynchronous setter. Collectively the promise and resolver are a deferred value.

The salient method of a promise is then, which creates a new promise for the result of a function that will eventually observe the value of the promise. If a promise were plural, the then method might be called map. If you care to beg an esoteric distinction, it might be called map if the observer returns a value and flatMap if the observer returns a promise. The then method of a promise allows either.

var promiseForThirty = promiseForTen.then(function (ten) {
    return ten + 20;
})

Promises can also have a done method that observes the value but does not return a promise nor captures the result of the observer. Again, if a promise were plural, the done method might be called forEach.

promiseForTen.done(function (ten) {
});

The then method also supports a second function that would observe whether the input promise radiates an exception, and there is a catch method to use as a shorthand if you are only interested in catching errors.

promise.then(onreturn, onthrow);
promise.catch(onthrow);

At this point, the design described here begins to differ from the standard Promise proposed for ECMAScript 6, arriving in browsers at time of writing. The purpose of these differences is not to propose an alternative syntax, but to reinforce the relationship between a promise and its conceptual neighbors.

A resolver is the singular analogue of a generator. Rather than yielding, returning, and throwing errors, the resolver can only return or throw.

resolver.return(10);
resolver.throw(new Error("Sorry, please return during business hours."));

With the standard promise, a free resolve function is sufficient and ergonomic for expressing both of these methods. resolver.return(promise) is equivalent to resolve(promise). resolver.return(10) is equivalent to resolve(10) or resolve(Promise.resolve(10))since non-promise values are automatically boxed in an already-fulfilled promise. resolver.throw(error) is equivalent to resolve(Promise.reject(error)). In all positions, resolve is the temporal analogue of return and reject is the temporal analogue of throw. Since promises as we know them today bridged the migration gap from ECMAScript 3 to ECMAScript 6, it was also necessary to use non-keywords for method names.

A deferred value can be deferred further by resolving it with another promise. This can occur either expressly through the resolver, or implicitly by returning a promise as the result of a observer function.

var authenticated = getUsernameFromConsole()
.then(function (username) {
    return Promise.all([
       getUserFromDatabase(username),
       getPasswordFromConsole()
    ])
    .then(function ([user, password]) {
        if (hash(password) !== user.passwordHash) {
            throw new Error("Can't authenticate because the password is invalid");
        }
    })
})

The then method internally creates a new deferred, returns the promise, and later forwards the return value of the observer to the resolver. This is a sketch of a then method that illustrates this adapter. Note that we create a deferred, use the resolver, and return the promise. The adapter is responsible for catching errors and giving the consumer an opportunity to do further work or to recover.

Promise.prototype.then = function Promise_then(onreturn, onthrow) {
    var self = this;
    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(value));
            } catch (error) {
                resolver.throw(error);
            }
        } else {
            resolver.throw(error);
        }
    });
    return deferred.promise;

The standard Promise does not reveal Promise.defer(). Instead, it is hidden by then and by the Promise constructor, which elects to hide the deferred object and the resolver object, instead "revealing" the resolve and reject methods as free arguments to a setup function, furthering the need to give these functions names that are not keywords.

var promise = new Promise(function (resolve, reject) {
    // ...
});

With a promise, information flows only from the first call to a resolver method to all promise observers, whether they are registered before or after the resolution.

With a task, information flows from the first call to a resolver method to the first call to an observer method, regardless of their relative order, but one kind of information can flow upstream. The observer may unsubscribe with an error. This is conceptually similar to throwing an error back into a generator from an iterator and warrants the same interface.

task.throw(new Error("Never mind"));

This interface foreshadows its plural analogue: streams.