[strawman] Symbol.thenable proposal

# Gus Caplan (7 days ago)

Hello all,

In an effort to curtail the interesting behavior of Promise.resolve (especially

with regard to dynamic import), I have created a proposal for a well-known

symbol which will allow an object to not be treated as a "thenable."

I am privy to the current protocol proposal which might be a better fit for

this, but due to dynamic import already being stage 3, I don't feel we should

wait for it to come to fruition.

Comments and suggestions are of course quite welcome at the repo 1.

Thanks,

-Gus

# Guy Bedford (6 days ago)

It's worth noting that the driving use case here is coming from NodeJS development hitting issues where the guaranteed result for dynamic import resolution can't be assumed to be a module namespace, although please correct me if I'm wrong here Gus.

Alternatively could this mitigation be handled by creating a Promise.resolveStrict primitive that explicitly opts-out of thenable resolution in the promise chain?

I'd think we are getting further and further away now from third-party promise implementation interops, that such approaches might make sense to consider at this point, in the name of returning to more well-defined semantics.

# Jordan Harband (6 days ago)

await import(path) wouldn't ever be able to do anything besides Promise.resolve; I'm pretty confident that this proposal, or something like it, is the only possibility to make ModuleRecords (for modules that export a then function) not be considered thenable.

# Isiah Meadows (6 days ago)

I can't remember where, but I recall seeing this discussed elsewhere (maybe in the TC39 meeting notes?) and the conclusion was basically ¯_(ツ)_/¯. I'm not convinced myself it's actually worth the extra symbol just to make something not considered a thenable - all these Promise libraries have been able to get away with it for this long; what makes ES promises any different here? (Dynamic import is probably the only possible case I can think of short certain proxies in terms of things that could be considered thenables but shouldn't always.)

Worst case, you can just return a value that happens to have a promise in a property, like in {value: somePromise} - nobody resolves that except co IIRC.

Isiah Meadows me at isiahmeadows.com

Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com

# Guy Bedford (5 days ago)

To state even more clearly how this is directly affecting API decisions in NodeJS, we're considering supporting a dynamic import hook for vm.Script, and one of the API suggestions is:

new vm.Script(import('x').then(x => console.log(x)), { async resolveDynamicImport (specifier) { return getNamespacefor(specifier); } });

The issue here being that this API is guaranteed to result in an error for module namespaces exporting a then function.

Yes, effectively not allowing variable dictionary object returns in promise-based APIs becomes the fix, but this does seem a rather arbitrary restriction on API development.

I think the proposal here would be a much-needed addition to be able lock-down guarantees in scenarios such as this.

# Michael Theriot (5 days ago)

Currently 'then' is effectively a reserved key and I'd expect any extension that allows the language to ignore it likewise include a means of changing the key entirely.

On Apr 12, 2018, at 9:33 PM, Gus Caplan <me at gus.host> wrote:

Hello all,

In an effort to curtail the interesting behavior of Promise.resolve (especially with regard to dynamic import), I have created a proposal for a well-known symbol which will allow an object to not be treated as a "thenable."

I am privy to the current protocol proposal which might be a better fit for this, but due to dynamic import already being stage 3, I don't feel we should wait for it to come to fruition.

Comments and suggestions are of course quite welcome at the repo 1.

Thanks, -Gus

# me at gus.host (5 days ago)

I don't understand why you would expect that, can you explain a bit more?

# Michael Theriot (5 days ago)

A hypothetical proposal addressing that would intersect with this.

# Gus Caplan (5 days ago)

It's a fair point, i assume the usage would be like if the property exists then ignore thenable behaviour and if it is falsy then none of any of this behaviour would occur, so a Module Namespace Object could have like [whateverSymbolName]: null, if thats sorta what you are proposing?

# Jordan Harband (5 days ago)

This proposal is that Symbol.thenable can be false, or true (which is a no-op). A future proposal could make it also be a string or symbol (IsPropertyKey), and then change the method name.

In other words, I don't think there's a conflict, and I think that changing the method name could and should be a followon proposal.

# Isiah Meadows (4 days ago)

Not sure how this is relevant, since this is about types rather than identity, and I'd say typing values is far from a solved problem beyond the basic concept of "all values have a type"...* ;-)

* en.wikipedia.org/wiki/Type_system


Isiah Meadows me at isiahmeadows.com

Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com

# doodad-js Admin (4 days ago)

I mean you could have an interface IThenable that you implement to a class/constructor. Then each instantiated object of such class/constructor can be "identified" as a Thenable. Yes, that involves a type system.

# me at gus.host (4 days ago)

An HTML attachment was scrubbed... URL: esdiscuss/attachments/20180415/6811e48a/attachment

# Tab Atkins Jr. (3 days ago)

On Fri, Apr 13, 2018 at 6:00 PM, Isiah Meadows <isiahmeadows at gmail.com> wrote:

I can't remember where, but I recall seeing this discussed elsewhere (maybe in the TC39 meeting notes?) and the conclusion was basically ¯_(ツ)_/¯. I'm not convinced myself it's actually worth the extra symbol just to make something not considered a thenable - all these Promise libraries have been able to get away with it for this long; what makes ES promises any different here? (Dynamic import is probably the only possible case I can think of short certain proxies in terms of things that could be considered thenables but shouldn't always.)

Having a reserved property key is a big footgun; you can't reliably resolve a promise to an arbitrary object, because if the object happens to have a "then" key, it'll try to recursively resolve it.

Userland may "get away with it" because "then" isn't a common key, but it's still a hassle, same as how proto being a special key in JS causes problems with JSON usage - it's rare, but not unknown, and problematic because it's an obvious layering violation.

Worst case, you can just return a value that happens to have a promise in a property, like in {value: somePromise} - nobody resolves that except co IIRC.

Note that this isn't about nesting promises (or even promise-likes) without recursive resolution, it's about nesting thenables into a promise without (attempted) recursive resolution, which is a much larger conceptual class: the presence of a "then" property says absolutely nothing about the type of an object. We've just been making a statistical judgement about the posterior probability of an object being promise-like based on the presence of a particular key, which is very dubious.

I'm still strongly of the opinion that we messed this up; the reason we went with thenables was because it's how userland did it, and they were working under the constraints of a proliferation of promise-likes and the difficulty of userland type-testing; real Promise usage promulgated much quicker than the pessimistic estimates, tho, making the relative trade-off of "free" compatibility with promise-likes vs the layering violation of reserving a property name on every object forever much less favorable. We should have instead relied on the actual Promise type, with a Symbol escape-hatch opting a userland object into being a promise-like. Doing the reverse and letting objects opt out of being treated as promise-like is probably the most we can do at this point, unfortunately. Generic data-handling will just have to manually add such a Symbol to objects they're resolving to, which sucks but is better than the alternative of always using a wrapper object.

(Note that having a "strictResolve" method won't help; it might prevent the precise promise you construct from recursing into the object, but if you then resolve another promise to that promise, it'll go ahead and try to recurse again. Maybe strictResolve() could auto-add the "I'm not a promise-like" symbol to the object it resolves to? That would be a bit more ergonomic for generic handling.)

# Ben Newman (3 days ago)

I agree with Tab that allowing developers to opt out of thenable behavior is the only recourse now, but I don't think Symbol.thenable is the best way to grant that power.

In the case of module namespaces, since module authors cannot use Symbol.thenable as the name of an export, there's no way to export a then function while also opting out of thenable behavior by exporting Symbol.thenable as false.

Instead, this proposal seems to imply that Symbol.thenable would have to be added automatically to every module namespace object, which could be bad news for module authors who (for whatever reason) actually wanted to export a then function to make the namespace thenable.

In general, whether or not we're talking about module namespace objects, we're beginning to realize that the presence of a function-valued .then property is not enough information to decide thenability in all cases, though of course it must remain the default behavior because that's how promise libraries have been written, and folks still do use promise libraries instead of the native Promise (e.g. Bluebird).

If an object does not have a .then function, then clearly it should not be treated as thenable, so I wonder if there might be a way to disambiguate individual then functions according to developer intent. Specifically, what if the developer could set object.then.able = false to prevent object from being treated as thenable, even though it has a function-valued then property? In addition to checking for the presence of a then method, Promise implementations would also need to check that then.able !== false before treating the object as thenable. If the .able property is absent from the then function (or truthy), the normal thenability behavior would continue to apply.

This proposal addresses the module namespace problem I described above, since you can set then.able = false before exporting then from a module. Note: because of function hoisting, it might be possible to get access to the then function before then.able has been set, but I don't think that's a problem for dynamic import(), since the module has to finish evaluating before the import() promise resolves.

While I think then.able is kinda cute (which is worth… something?), I would also be totally open to alternatives like setting then[Symbol.thenable] = false. The essential idea is to use properties of the then function itself (rather than sibling properties of the object) to indicate whether the object should be thenable.

Ben

His errors are volitional and are the portals of discovery. -- James Joyce

# Jordan Harband (3 days ago)

I think the purpose of this proposal is to forbid module authors from making their Module Record namespace object thenable, since doing that causes confusion.

Setting a property on the function doesn't work when the "then" is inherited but the object still wants to be non-thenable; I think that the property has to go on the object - the thing that's not thenable - not on the "then" function itself, which wouldn't even have to be touched or looked at if the object is non-thenable.

# T.J. Crowder (2 days ago)

On Mon, Apr 16, 2018 at 6:36 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

I'm still strongly of the opinion that we messed this up... Doing the reverse and letting objects opt out of being treated as promise-like is probably the most we can do at this point, unfortunately.

I was going to play devil's advocate here ("Is it really too late? Probably, but we should at least explore it..."), toying with the idea of a more strict kind of promise (I called it NativePromise), but I ran into a wall which may affect Symbol.thenable = false as well:

someThirdPartyPromise.then(() => import("foo")).then(/*...*/);

That converts the promise from import() (which understands Symbol.thenable = false) into a non-native Promise, and thus triggers the bug Symbol.thenable is meant to resolve, since when import()'s promise resolves, it hands a thenable to the 3rd-party promise that doesn't handle Symbol.thenable = false. Boom.

How does this proposal address that? Because if it doesn't, and third-party libs are supposed to update to support Symbol.thenable = false, we should at least discuss making promises opt-in via Symbol.thenable = true for a special class or type of promise.

My thought (which, again, ran into the wall above) was: Add a new type of promise (call it NativePromise) which doesn't support thenables, just objects with Symbol.promise = true (note I didn't call it "thenable"). (I also toyed with well-known symbols for then, catch, and finally instead, but that's probably overkill). All language-generated and host-provided promises would be NativePromises (this would be a breaking change for async functions, thus impact assessment would be required). Notes:

  • import() would use NativePromise and thus not be confused by a module exporting a then function
  • NativePromise would be thenable
  • Promise would continue to support thenables
  • NativePromise would not, and thus would not be Promises/A+-compliant
  • NativePromise and Promise would be fully interoperable (both would have Symbol.promise = true and then)
  • Actively-maintained 3rd-party promise libs like Bluebird would implement Symbol.promise = true to get full interoperability with NativePromise
  • NativePromise would only have one-way interoperability with any 3rd-party lib that didn't implement Symbol.promise = true

Again, that's where I ran into the wall above, which I think impacts Symbol.thenable = false as well.

-- T.J. Crowder

# me at gus.host (2 days ago)

An HTML attachment was scrubbed... URL: esdiscuss/attachments/20180417/2c899a03/attachment