Remove "thenable" from the spec

The Promises/A+ spec is a disaster. There are two main things that are wrong with it.

  1. Combining map and flatMap in a single method.
  2. Assimilating objects which happen to have a then method.

The first problem is a minor pain point because it only affects promises. However, the second problem is a major pain point because it affects the entire JavaScript ecosystem.

Assimilation of objects which happen to have a then method is such a terrible design that I conscientiously refuse to use the word "thenable". There is no such thing as a "thenable". There are only objects which happen to have a then method.

Here are some of the things that people said about the assimilation of objects which happen to have a then method, before it made it into the ECMA-262 specification.

  • robotlolita commented on Apr 11, 2013:

    I'm okay with assimilating promises when you return a promise from a function in an on{Fulfilled,Rejected} , I'm not okay with assimilating things with a then function. That it hasn't happened until now doesn't mean it will never happen (potential ≠ guaranteed).

  • polotek commented on Apr 12, 2013:

    Asking me to "fix my library" presumes that you've decided it's broken. It also presumes that I would agree with you that it's broken. My library works as designed for the most part. It's even got some tests on it (non-exhaustive as they may be). The point of contention here seems to be my use of the names "promise" and "then" in my design.

    […] I think it's a bit disingenuous to suggest to me and everyone else who ever uses the names "promise" and "then" that they should conform to A+. Even considering that I agree with what you're trying to accomplish here. There are several reasons for this. One is the presumption that I share your goal of interoperability. "until @polotek fixes his library, it can't co-exist with various promise libraries". Procstreams weren't designed to co-exist with promise libraries. No one has asked for it. I'm not sure how it has somehow become a requirement for your design, or how subsequently fixing that is my problem. Or maybe I'm misunderstanding your point. Are you saying that the promises A+ spec cannot and should not try to accomodate all Thenables in js? Because that I agree with.

  • getify comment on Apr 12, 2013:

    I consider it utter hogwash for anyone advocating for Promises/A+ that anyone else who has ever built a lib with a then() method now needs to change their API to make way for the freight train (called Promises/A+) that is barreling their way. Of all the blustering in this thread, and I just read the whole thing, it was that notion that completely "lost" me.

    There's (I'm sure) at least several libs in the pantheon of JS which have used the then() method name and they're not promises compliant (or assimilable by a promise in a way that wouldn't constitute surprise behavior by the person who was used to different behavior before introducing Promises/A+ into the mix).

    For reference, I have been doing "promise like" coding in JS for well over 3 years now. I am quite certain I was experimenting with these ideas well before Promises/A+ came along, and probably well before a lot (not all) on this thread were. And I have written promise libs using the then() method way before you decided to "reserve" that name.

    My current works in the promises area are asyncGate and asyncSteps, both of which abstract lightly the concept of promises, but provide simple(r) APIs for managing async code flow. Both of these libs were ALSO written before Promises/A+ came along. And both of them use then() . I'm not 100% positive, but I am pretty sure my libs are not A+ compliant and that they would behave in unexpected/surprising ways if incidentally assimilated by a A+ lib.

    I don't relish that my libs will forever be banished as incompatible (and broken by) from your way of viewing the promises world. But note: I am not the one who's creating the incompatibility, "you" are. My libs definitely wouldn't reject or misbehave if they saw one of your objects floating by. If you had any goal of trying to get any of us on the fringe on board, this tact was definitely not the way of doing so.

These quotes show that when the process of "assimilating objects which happen to have a then method" was added to the specification, it broke a lot of existing libraries. That is, it was a BREAKING CHANGE. The tight coupling of promises with the then method ensured that no other library could ever use the method name then for any other purpose.

  • Existing libraries had to come up with a different name to be Promises/A+ compatible.
  • Newer libraries also had to square with this issue.
  • There's even a custom eslint rule to prevent methods being named then.

All of these problems are caused by a wrong decision made 10 years ago. We shouldn't have to live with the consequences of wrong decisions for all eternity. Let's fix this by removing the process of "assimilation of objects which happen to have a then method" from the spec.

Is this a breaking change? Yes, it most definitely is. However, it's a necessary change. To quote Malcolm X:

If you stick a knife in my back nine inches and pull it out six inches, there's no progress. You pull it all the way out, that's not progress. The progress is healing the wound that the blow made.

Let's not forget that the behavior of assimilating objects that happen to have a then method was itself a breaking change. It was met with a lot of resistance from the JavaScript community, but added to the spec nonetheless. So, having another breaking change to rectify the mistake is not radical. It's progressive.

Once we agree that this is indeed a problem that needs to be solved, we can discuss the specifics of the solution. For example, we could talk about:

  • Using a symbol such as Symbol.then to brand an object that should be assimilated.
  • Or, removing the assimilation of non-promise objects from the specification entirely.

Finally, I just want to leave you with one thought.

We don't have to live with mistakes. We can and should correct them.

Breaking changes in JS are usually about when existing code, exactly how it is currently written/deployed stop working as intended as the runtime gets updated with newer features.

I don't think that applies as much when Promises were initially added? Would you be able to expand on that?

We seem to have different definitions of what a breaking change constitutes.

  • It seems to me that your definition of a non-breaking change only accounts for application code written previously still working as intended.
  • However, my definition of a non-breaking change is more holistic. It also accounts for APIs written previously not needing to be updated to work as intended with the new changes.

Before promises were originally added, if a library exposed a then method that wasn't compliant with Promises/A+ then the maintainers would have to update the library to make it compliant. The fact that the library can't be used as is, and needs to be updated to correctly integrate with promises, indicates that Promises/A+ introduced a breaking change.

The Promises/A+ specification didn't break application code that was deployed at that time. However, it did break libraries which exposed a then method that wasn't compliant with the Promises/A+ specification. The effect of that breaking change was only felt later when those libraries were integrated with promises and didn't work as expected.

All of this is caused because promises are tightly coupled with the then method. The Promises/A+ specification makes several (incorrect) assumptions about what "an object which happens to have a then method" does. And, as the saying goes, "when you assume you make an ass out of you and me." So, when this assumption invariably fails a lot of existing libraries don't work with the built-in language features anymore and need to be updated.

That's the breaking change. It breaks libraries which were written before Promises/A+ came along and unofficially reserved the name then for itself. And, by adding the Promises/A+ specification to ECMA-262, those libraries were no longer compatible with the new features of the language. And the expectation was that those libraries should change, instead of Promises/A+ playing nicely with the rest of the ecosystem.

Just look at how hostile some of the comments were from that thread.

  • Don't define then methods that don't conform to the Promises/A+ spec. If you want a then-like method with a different behavior, call it something else.

    Source

  • @polotek Please fix your Node library. Thanks in advance.

    @killdream yes, until @polotek fixes his library, it can't co-exist with various promise libraries. It is far too late to do anything to fix that. The evolution of JavaScript has always been and remains constrained by its history of usage.

    Source

This ship has long since sailed and isn't worth discussing. then is poisoned, forever, unchangeably.

2 Likes

I agree that the concept of thenable was a bad choice.

But there's two types of breaking changes we're discussing here.

There's "breaking" in the sense that, any existing code continues to work, but there's a strong ensensitive for library authors to update their API, because it doesn't play well with of some JavaScript's newer features. This isn't ideal and should be avoided, but it does happen from time to time. We also saw this with async/await syntax - angular exposed a function named await, that they have since deprecated in favor of some other function that was exactly the same, but had a different name. They did this, so their API wouldn't conflict with the new await syntax. The await syntax didn't break any existing angular code, but it did strongly encourage Angular to make changes to their API. It's good to avoid changes of this nature, so I agree that it would have been better if thenable was never a thing, at least with how it got designed.

Then, there's "breaking change" as in, this feature being added is literally going to break existing websites. This is always a no-no. No browser is going to agree to a change in a language, that will force them to not be able to properly render certain webpages anymore. Why would they want to do that? If such a chance were proposed and 3/4ths of the browsers adopted it, but one decided not to, then anyone who needs to use those old webpages will start using the backwards compatible browser instead. (Edit:) Even if they were all on board with the change, I would very much dislike it - I'd like to be able to view any old webpage on the way back machine and what not.

So, we're forever stuck with thenables, because removing them will cause existing webpages to stop working. Just like we're forever stuck with the equally bad toString and toJSON protocols. If only symbols were invented earlier, so we didn't have to use string properties for all of these protocols...

How about somehow marking an object which happens to have a then method as exempt from assimilation by the promise resolution procedure? For example, consider the following.

class Option {
  constructor(option) {
    this.option = option;
  }

  static {
    this.prototype[Symbol.isThenable] = false;
    this.None = new Option(null);
  }

  static Some(value) {
    return new Option({ value });
  }

  then(that) {
    return this.option === null ? this : that;
  }

  when(that) {
    return that.option === null ? that : this;
  }
}

The above Option class has a then method. However, we're explicitly stating that it's not a thenable by setting @@isThenable to false on the Option.prototype.

This means that the following code will resolve as expected instead of never resolving:

await Promise.resolve(Option.None);

There's already prior art of such control coupling. See, Symbol.isConcatSpreadable.

It's not an ideal solution. Adding more coupling to correct a mistake caused by tight coupling. However, it's backward compatible and it doesn't break the web.

2 Likes

This was tried, and rejected by the committee: https://github.com/tc39/notes/blob/ace580d512db32624bd74b843e0d9757278753cd/meetings/2018-05/may-24.md?plain=1#L808

Again, no changes to then are possible.

1 Like