Skip to content

Instantly share code, notes, and snippets.

@briancavalier
Created October 23, 2012 15:18
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save briancavalier/eb5fc157825a170c9957 to your computer and use it in GitHub Desktop.
Save briancavalier/eb5fc157825a170c9957 to your computer and use it in GitHub Desktop.
Promises/A+

Promises/A+

This proposal attempts to clarify the behavioral clauses of the Promises/A proposal, and to extend it to cover the cases where handlers may return a promise.

This proposal intentionally omits the progress handling portion of Promises/A. In practice it has proven to be underspecified and currently does not have an agreed-upon or defacto behavior within the promise implementor community.

Also intentionally omitted is a requirement for calling fulfill and broken handlers either synchronously or asynchronously [1]. Promises/A itself does not specify, and both synchronous and asynchronous approaches exist in the current landscape of popular implementations.

This specification borrows heavily from the Promises/A proposal by Kris Zyp, as well as the UncommonJS Thenable Promises specification by Kris Kowal. All credit goes to those authors.

As with Promises/A, this proposal does not deal with creation of promises.

Promise

A promise represents a value that may not be available yet. A promise must be one of three states: pending, fulfilled, or broken:

  • When in the pending state, a promise may transition to either the fulfilled or broken state.

  • When in the fulfilled state, a promise has a value and provides a way to arrange for a function to be called with that value. Once a promise has transitioned to the fulfilled state, it must never transition to any other state.

  • When in the broken state, a promise has a reason (an indication of why it was broken) and provides a way to arrange for a function to be called with that reason. Once a promise has transitioned to the broken state, it must never transition to any other state.

Requirements

A promise is an object or function that defines a then method that accepts at least 2 arguments:

promise.then(fulfilled, broken)
  • Both fulfilled and broken are optional arguments

  • If truthy, fulfilled must be a function that accepts a value as its first argument.

    • When promise is fulfilled, fulfilled will be called with promise's fulfillment value.
    • fulfilled will never be called more than once.
    • fulfilled will never be called if broken has already been called.
  • If truthy, broken must be a function that accepts a reason (which must be a value, not a promise) as its first argument.

    • When promise is broken, broken will be called with promise's reason for being broken.
    • broken will never be called more than once.
    • broken will never be called if fulfilled has already been called.
  • then may be called any number of times.

  • fulfilled and broken supplied in one call to then must never be called after those supplied to a later call to then on the same promise.

  • then must return a promise [2]

      var promise2 = promise1.then(fulfilled, broken)
    
    • When promise1 is either fulfilled and fulfilled is called with the fulfillment value, or broken and broken is called with the reason:
      • If either returns a value, promise2 must be fulfilled with that value.
      • If either throws an exception, promise2 must be broken with the thrown exception as the reason.
      • If either returns a promise (call it returnedPromise), promise2 must be placed into the same state as returnedPromise:
        • If returnedPromise is fulfilled, promise2 must be fulfilled with the same fulfillment value.
        • If returnedPromise is broken, promise2 must be broken with the same reason.
        • If returnedPromise is pending, promise2 must also be pending. When returnedPromise is fulfilled, promise2 must be fulfilled with the same fulfillment value. When returnedPromise is broken, promise2 must be broken with the same reason.

Recommendations

  1. Each implementation should document whether it calls handlers synchronously or asynchronously.
  2. Each implementation should document whether it may produce promise2 === promise1, and if so, under what conditions. It is intentionally not specified as to whether the returned promise may be the same promise, or must be a new promise, i.e. promise2 !== promise1 is not a requirement. An implemention is free to allow promise2 === promise1, provided it can meet the requirements in this section.
@lsmith
Copy link

lsmith commented Oct 23, 2012

This looks like @kriskowal's Thenable Promises section almost verbatim. Can you point out the distinctions?

Preamble

  • I like the note about underspecification of progress.
  • I'm on the fence about this, but I think you might include specification of asynchronous handler execution, or at least initiation of the handler notification process. So when resolve() (or whatever triggering method) is called, either each or all handlers are executed off thread. This allows consistent behavior for executing resolvedOrUnresolvedPromise.then(doStuff).
  • Worth specifying that multiple fulfillment/broken handlers will be invoked in sequence irrespective of those handlers returning promises? That is, handler2 will be invoked after handler1 is invoked even if handler1 returns a promise. It seems to be the assumed consensus, but I haven't seen it documented anywhere.
  • "As with Promises/A, this proposal does not deal with creation of promises." or the methods to fulfill or break created promises. Though you might include a note that it is recommended that these methods not exist on the promise object itself. Or if that's beyond the scope of the doc, to include it in the omissions list (which may start looking lengthy).

Promise

  • I like the terminology used in Thenable Promises of "unresolved" rather than "pending", though there's opportunity for confusion with the resolve() method of Promise Managers or Deferreds. Naming discussion! But may be important.
  • Consider moving the "Once a promise has ..." sentences to the requirements section, and use MAY, MUST, or MUST NOT to leverage existing spec language.

Requirements

  • Use MAY, MUST, or MUST NOT to leverage existing spec lingo.
  • The use of the word "single" here makes it look like a promise must be an object with only a then() method; no other methods allowed. Am I misunderstanding? This is clean for the purpose of separation, but would likely be ignored in implementation, as it's not conducive to API sugaring. Imagine an ajax related api that generates promises with a parseJSON method that desugared to this.then(function (response) { return JSON.parse(response.responseText); }) or an animation related promise factory that generated promises with a wait(msDelay) method.
  • Maybe better to spec that implementations MAY allow a third argument for progress handler? "...that accepts at least two arguments" is nice for those of us that don't like progress being in there, but could open the door to additional args like context override and additional callback arguments. If this is intentional, I'm not sure if it's worth documenting example options, but recognize that it's an opportunity for implementation fragmentation that could jeopardize cross-lib compatibility.
  • You should document the value/reason relaying behavior when fulfilled or broken is not provided. This is especially relevant for middle steps in a promise chain.
  • Specify behavior for resolvedPromise.then(what, now)
  • Worth specifying that promises MUST allow duplicate handlers?
  • Specify what happens when fulfilled or broken don't return a value.
  • Do you know of implementations that produce promise2 === promise1? I can't imagine a use case that would require that behavior. It seems wrong.

@domenic
Copy link

domenic commented Oct 23, 2012

Haha I was totally thinking of doing this, with exactly this name too! Glad you are on top of it; it's great to have others sharing the effort :)


I actually like some of the terminology things going on here:

  • Promises are never resolved; this spec doesn't mention that term. However, deferreds can be resolved. If we use the term "resolved" as a promise state (meaning "non-pending"), it's confusing, since you can resolve a deferred with a pending promise.
  • Broken always made more sense to me. If we keep "reject" as the deferred's verb, and adopt "broken" as the promise state, then we've entirely managed to separate the terminology used by promises and deferreds, which helps reduce the confusion.

That is, promises are either pending, fulfilled, or broken. Deferreds can be resolved or rejected. Now we can explain how the latter impacts the former, without being confused by the way that, in the old terminology (pending vs. resolved and fulfilled vs. rejected) "reject" maps rather directly where "resolve" is rather complex.


I feel somewhat strongly that async resolution should be required for compliance. It can be turned off for specific libraries as an optional feature, but the default configuration of most libraries should be async. I realize this would be a breaking change for When, requiring users to specify an option to retrieve the old behavior, but I hope that would be OK. Pinging @wycats.


You say "must be a function," but that's rather imprecise. Should then throw an early error? Ignore it? Store it in the internal listeners array, then blow up when calling a non-function.


Given that the then duck-type test is easy to fool, and has existing landmines in the wild, I'd consider specifying a better method of detection. For example, AMD has something like define.amd. I'd love then.aplus or similar.

@domenic
Copy link

domenic commented Oct 24, 2012

You should document the value/reason relaying behavior when fulfilled or broken is not provided. This is especially relevant for middle steps in a promise chain.

+1

Specify behavior for resolvedPromise.then(what, now)

+1; the current "when promise is fulfilled" and "when promise is broken" are not clear enough. (Although, as per my above comment, I recommend doing away with the terminology "resolved promise".)

Specify what happens when fulfilled or broken don't return a value.

+1; amending "returns a value" with "returns a value (including undefined)" should suffice.

Do you know of implementations that produce promise2 === promise1? I can't imagine a use case that would require that behavior. It seems wrong.

domenic/promise-tests#2 discusses this; in particular see this comment.

Maybe better to spec that implementations MAY allow a third argument for progress handler?

Probably a good idea, or something like this. Maybe "if implementations use a third argument, it MUST be a progress handler"? It's nice being able to absorb progress-emitting when promises into Q (via thenable.then(deferred.resolve, deferred.reject, deferred.notify)) without worrying that someone might create a new library that takes as its third parameter something completely different.


I also think we should produce Deferreds/A+, but let's leave that discussion for another time.

@kriskowal
Copy link

I don’t have any opinions that have not yet been represented.

  1. I, and the experts I learned promises from, feel strongly that sync and async should not be mixed. Since a promise by definition may be async, it follows that they must always be async. when.js is welcome to play with that fire, but I will not be raising my flag on that particular aspect. That said, I would not want to miss an opportunity to converge on all the things we can agree on in terms of terminology and behavior. I propose that Promises/A+ be strictly async, and when.js can emblazon itself as an almost-compliant variant pointing out that caveat.
  2. As far as I’m concerned "pending" and "unresolved" are equivalent and opposite to "resolved". "resolved" can mean "fulfilled" or "rejected". Resolvers confuse the issue since "resolve" (in Q terminology) does not imply that the promise becomes "resolved", but I don’t have a good solution to that problem. As far as I’m concerned "reject" and "break" are equivalent, and "rejected" and "broken" are equivalent. I believe ref_send’s Q used "reject" to avoid a collision with the "break" keyword. Going forward, we might revisit that decision.
  3. I don’t think we should specify anything more than "then" to identify a compliant library. Anything that has a "then" method that does not pertain to promises should be wrapped. @domenic, we should talk about this more; Q could provide a facility for unconditionally wrapping, possibly through Q.fulfill.
  4. Give a mouse a cookie. I think it would be grand if we agreed on this much. It would be a basis for run-time equivalence. It would be even better to agree on the "defer()" -> {resolve, promise} interface too, so libraries could be swapped.
  5. Talked a bit to @wycats and managed to articulate that {promise, resolver} might be more appropriate than {promise, resolve}. The {resolver} would be a passable object with all of the state change methods {resolve, reject, notify, cancel}.

@wycats
Copy link

wycats commented Oct 24, 2012

I, and the experts I learned promises from, feel strongly that sync and async should not be mixed. Since a promise by definition may be async, it follows that they must always be async. when.js is welcome to play with that fire, but I will not be raising my flag on that particular aspect. That said, I would not want to miss an opportunity to converge on all the things we can agree on in terms of terminology and behavior. I propose that Promises/A+ be strictly async, and when.js can emblazon itself as an almost-compliant variant pointing out that caveat.

Fully agree.

As far as I’m concerned "pending" and "unresolved" are equivalent and opposite to "resolved". "resolved" can mean "fulfilled" or "rejected". Resolvers confuse the issue since "resolve" (in Q terminology) does not imply that the promise becomes "resolved", but I don’t have a good solution to that problem. As far as I’m concerned "reject" and "break" are equivalent, and "rejected" and "broken" are equivalent. I believe ref_send’s Q used "reject" to avoid a collision with the "break" keyword. Going forward, we might revisit that decision.

I like the nomenclature of "promise" and "resolver", but I am not sold on "broken" as a state. It strikes me as too cute. I'm not sure I understand why "rejected" doesn't work as the adjective form of resolver.reject.

I don’t think we should specify anything more than "then" to identify a compliant library. Anything that has a "then" method that does not pertain to promises should be wrapped. @domenic, we should talk about this more; Q could provide a facility for unconditionally wrapping, possibly through Q.fulfill.

In my use cases, it is important to be able to detect whether some object is a promise or not. Some of my APIs support promises as an advanced feature only, allowing users to return regular values in place of promises if desired. A mostly-foolproof way to identify a promise would be great. I like the "branding" approach of `promise.then && promise.then.aplus.

Give a mouse a cookie. I think it would be grand if we agreed on this much. It would be a basis for run-time equivalence. It would be even better to agree on the "defer()" -> {resolve, promise} interface too, so libraries could be swapped.

I really don't like the use of defer here. It's inappropriately a verb. I don't like Foo.deferred() any better, because it creates a new concept ("a deferred"), which the rest of the nomenclature has (thank god) been able to avoid. And while Cocoa folks might like Foo.promiseAndResolverPair(), it's probably too verbose ;)

I'm open to other options.

Talked a bit to @wycats and managed to articulate that {promise, resolver} might be more appropriate than {promise, resolve}. The {resolver} would be a passable object with all of the state change methods {resolve, reject, notify, cancel}.

This seems like another spec? "Promises/A+ Resolvers"? There's a less urgent need for compatibility and more generally evolving state of the art here (e.g. what should happen when a promise is cancelled/aborted), so we can take our time on this one, I'd think.

@domenic
Copy link

domenic commented Oct 24, 2012

As far as I’m concerned "pending" and "unresolved" are equivalent and opposite to "resolved". "resolved" can mean "fulfilled" or "rejected". Resolvers confuse the issue since "resolve" (in Q terminology) does not imply that the promise becomes "resolved", but I don’t have a good solution to that problem.

Well, by removing the term "resolved" from promises, I was hoping to solve this problem.

I don’t think we should specify anything more than "then" to identify a compliant library. Anything that has a "then" method that does not pertain to promises should be wrapped. @domenic, we should talk about this more; Q could provide a facility for unconditionally wrapping, possibly through Q.fulfill.

I'm not quite sure I understand what you're suggesting. My use case is to improve this check:

if (typeof assertion._obj.pipe === "function") {
  throw new TypeError("Chai as Promised is incompatible with jQuery's so-called “promises.” Sorry!");
}

If I could replace it with

if (!assertion._obj.then.aplus) {
  throw new TypeError("Chai as Promised only works with Promises/A+ compatible promises. Sorry!");
}

I'd feel much better.

Talked a bit to @wycats and managed to articulate that {promise, resolver} might be more appropriate than {promise, resolve}. The {resolver} would be a passable object with all of the state change methods {resolve, reject, notify, cancel}.

I really like this, actually. It makes the separation I allude to while trying to explain resolution races more apparent.

@domenic
Copy link

domenic commented Oct 24, 2012

I'm not sure I understand why "rejected" doesn't work as the adjective form of resolver.reject.

Because "resolved" doesn't work as the adjective form of resolver.resolve.

@wycats
Copy link

wycats commented Oct 24, 2012

Because "resolved" doesn't work as the adjective form of resolver.resolve.

Why not reject/rejected and fulfill/fulfilled?

@domenic
Copy link

domenic commented Oct 24, 2012

Why not reject/rejected and fulfill/fulfilled?

Because we need a way to say resolver.adoptEventualStateOf(pendingPromise). Currently it's deferred.resolve(pendingPromise).

@briancavalier
Copy link
Author

Wow, I go out for the evening and you guys post all sorts of great feedback :) It'll take me a bit to get through all of this, but I'll try to do so today.

@briancavalier
Copy link
Author

I think we'll probably benefit from the better history, diff, and commenting tools in a full repo, so here we go. I also created this issue where we can continue. Sorry to have to break up the conversation, but I figured may as well do it now than when we've amassed even more comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment