00:02
<bakkot>
ah
00:02
<bakkot>
the problem specifically being that Promise.prototype.then does the SpeciesConstructor stuff, I suppose
00:03
<bakkot>
otherwise you could just use Promise.prototype.then.call(val) as your test
00:03
<Mathieu Hofman>
and Promise.resolve, yes
00:05
<Mathieu Hofman>
There is no place in the spec that does IsPromise without also poking at .constructor or .then on the object.
00:06
<bakkot>
perhaps we will manage to remove Symbol.species instead
00:06
<Mathieu Hofman>
I don't think it'll help. .constructor based species is not slated for removal from what I recall
00:09
<bakkot>
that's Type II in the taxonomy in the proposal, and it is proposed to be removed per https://github.com/tc39/proposal-rm-builtin-subclassing#proposed-new-old-semantics
00:09
<bakkot>
I don't think we'll actually manage it but I live in hope
00:10
<Mathieu Hofman>
I'm curious if there would be any opposition to basically have a Promise.isPromise(x) which basically does IsPromise(x). I know it'd solve my problem
00:11
<bakkot>
I think there would need to be more motivation than your very narrow use case
00:12
<bakkot>
and I'd be hard pressed to come up with such motivation given that the actual thing one usually cares about is "is thenable"
00:13
<Mathieu Hofman>
The thing is that it's impossible to workaround currently, and I fail to see how exposing something that already exists as a new namespaced API has much complexity.
00:15
<Mathieu Hofman>
I am sure ljharb would love a clean way to brand check a promise for his libraries ;)
00:15
<bakkot>
People would use it instead of "is thenable" and be confused
00:16
<bakkot>
"is promise" is almost never the test anyone actually wants; you and ljharb have extremely unusual use cases
00:16
<bakkot>
but it looks like a thing you might want, so people will use it in other cases, and that's bad
00:19
<Mathieu Hofman>
Do you have any suggestion to make it more obvious what this does, or less likely to be misused?
00:19
<Kris Kowal>
I’m sure there’s a way to express the brand-check that would be clear and sufficiently out of reach.
00:21
<Kris Kowal>
I agree that there’s potential for confusion for any particular behavior for Promise.isPromise(thenable).
00:22
<Kris Kowal>
And I also agree we need to be able to check whether a value is a native promise without reentering any of its API.
00:24
<Kris Kowal>
Though, Promise.isPromise(thenable) === false isn’t that weird of an answer. It’s not a Promise yet, even if it’s a promise with a little P.
00:26
<Kris Kowal>
The notion that Promise.isPromise(X) === true only for X that can become a promise invites the possibility that "hello" is a promise.
00:35
<bakkot>
IME the main reason people use isArray is to make polymorphic APIs, where e.g. you take either a list of names or a single name. and I expect that's the main way people would use this as well: if (Promise.isPromise(x)) x = await x, say. but that doesn't do the right thing if x is thenable but not a Promise.
00:36
<bakkot>
this seems bad. especially for people who want to make proxies for stuff unless we make isPromise pierce proxies like isArray does, which we definitely will not do
00:36
<ljharb>
in practice tho it’s rarely a thenable
00:36
<Mathieu Hofman>
I would not expect proxies to pierce this
00:36
<Kris Kowal>
The example is not coherent because await 1 works fine, but 1 is not a promise.
00:36
<bakkot>
the example is perfectly coherent
00:36
<ljharb>
non-Promise thenables largely died once Promise and async/await became commonplace
00:36
<bakkot>
ljharb: depends on how many proxies you have around
00:36
<bakkot>
proxy for a promise is thenable but not a promise
00:37
<ljharb>
“Any use of proxy” is incredibly niche automatically :-)
00:37
<bakkot>
fair enough
00:37
<bakkot>
Kris Kowal: how is the example not coherent?
00:37
<bakkot>
I mean, it's wrong, because it's treating "thenable" as the same as "is promise", but I guarantee people are going to do that; that's my point
00:38
<bakkot>
if we didn't have thenables and only had promises, the example would be correct for a polymorphic API which wants to skip the await when passed a value which is not boxed in a promise
00:38
<Mathieu Hofman>
I agree that using isPromise with the intent to skip an await would be misguided.
00:38
<bakkot>
which is a thing people do in fact want to do
00:38
<bakkot>
constantly
00:38
<ljharb>
not just polymorphic; these kinds of checks are always useful for helpful runtime validation of input types
00:39
<ljharb>
which includes an IsPromise check, which thus isn’t quite as niche as claimed
00:39
<Mathieu Hofman>
currently people do Promise.resolve(val) === val, but that can synchronously trigger user code attached to val
00:40
<ljharb>
Which is absurdly rare, so it’s ‘good enough’ but still not perfect
00:40
<bakkot>
I am fine with this
00:40
<Kris Kowal>
I’m drawing a blank on what sensible thing one would be achieving withif (Promise.isPromixe(x)) await x that would not be achieved by await x.
00:40
<bakkot>
skipping the await when passed a value which is not boxed in a promise, like i said
00:40
<ljharb>
Kris Kowal: also telling someone “i wanted a promise here, and you gave me something else, so you probably have a bug”
00:41
<ljharb>
if i expect “an awaitable” then it doesn’t matter, and if i accept a thenable then I’d check for .then being a function ¯_(ツ)_/¯
00:41
<Kris Kowal>
Indeed.
00:41
<ljharb>
but virtually nobody expects a thenable - they expect a Promise.
00:42
<bakkot>
false
00:42
<bakkot>
I have absolutely no problem with someone handing me a proxy for a promise
00:42
<Mathieu Hofman>
What if we introduce both a isPromise and isThenable, then people wouldn't be as confused ?
00:42
<bakkot>
or a different-realm promise
00:42
<bakkot>
those things are fine
00:42
<ljharb>
intentionally i mean. A thenable works perfectly fine ofc
00:42
<bakkot>
Mathieu Hofman: that would be... more confusing
00:42
<ljharb>
and “a different realm” is almost as rare as proxy
00:43
<Jessidhia>
is it still common for people to use <favorite A1 implementation> instead of native Promise or a polyfill
00:43
<bakkot>
people do still use bluebird pretty often, yes
00:43
<bakkot>
not as much as they used to
00:43
<ljharb>
mostly by inertia, not for a good reason
00:43
<Mathieu Hofman>
Mathieu Hofman: that would be... more confusing
How so?
00:44
<bakkot>
"thenable" means "has a callable .then property", which is a thing you can check directly; the existence of a method which does this implies it is something else
00:44
<Mathieu Hofman>
is it still common for people to use <favorite A1 implementation> instead of native Promise or a polyfill
React Native for example forces on a promise polyfill (bluebird from what I recall). I'm incredulous why, but that's one example of weird always on non-native promises
00:45
<ljharb>
weird
00:45
<Kris Kowal>
Q’s still getting an absurd weekly download figure, for what it’s worth.
00:45
<bakkot>
so, right. bluebird and Q are good examples of why normal users don't actually want IsPromise in the example I gave, and giving them IsPromise would lead them to have the wrong behavior.
00:45
<bakkot>
so, we should not have IsPromise.
00:45
<ljharb>
you could probably hunt through dependents and figure out how much of those numbers are due to new usage
00:46
<ljharb>
normal users don’t use bluebird or q directly, i claim, and thus have no desires for them
00:46
<Jessidhia>
avoiding awaits seems very misguided IMO, that’s just borderline attempting to revive the Zalgo problem
00:46
<bakkot>
I don't know what "thus have no desires for them" is doing in that sentence
00:46
<Kris Kowal>
Also, Q.isPromise is real and does a brand check of Q promises.
00:46
<ljharb>
(I’m not expecting IsPromise to ever be exposed, ofc)
00:46
<bakkot>
you might end up with a bluebird promise because someone else gave you one
00:46
<bakkot>
and you shouldn't trip over and die as soon as that happens
00:47
<bakkot>
you should just use it like any other promise
00:47
<bakkot>
and life will be good
00:47
<ljharb>
I don't know what "thus have no desires for them" is doing in that sentence
i mean that the existence of those libs in transitive deps doesn’t prove people want them. They probably just don’t realize they’re there
00:47
<bakkot>
yeah but
00:47
<bakkot>
they are there
00:47
<bakkot>
and they don't have to realize it
00:47
<bakkot>
specifically because we don't have IsPromise
00:47
<bakkot>
which is... good
00:47
<ljharb>
sure. But as an API producer i have the right to force them to realize it if i want.
00:48
<ljharb>
and i can do that now already. Just imperfectly.
00:48
<bakkot>
I don't know what "have the right" means
00:48
<Jessidhia>
I remember that for a good while bluebird had better DX than native; I haven’t revisited this in years though
00:48
<bakkot>
you can write whatever code the language allows you to write, yes
00:48
<ljharb>
lol i mean, i can do it, and it’s not a bad thing if i do it
00:48
<bakkot>
I'm not going to pass judgement on "a bad thing"
00:49
<ljharb>
right - so i think while the argument in favor of the predicate is weak, the argument against it is also weak
00:49
<bakkot>
my argument has nothing to do with whether API consumers have the right to deliberately reject bluebird promises if they are so inclined
00:49
<ljharb>
iow “non-Promise thenables exist” isn’t a good argument imo against IsPromise
00:49
<bakkot>
my claim is that, for a normal user, who is doing the totally normal thing of trying to skip an await when given something which is not boxed, IsPromise would lead them to doing the wrong check, because that would lead them to skipping bluebird promises, which is not something they are specifically trying to do
00:50
<ljharb>
perhaps. How can we know that unless they learn the difference?
00:50
<Mathieu Hofman>
(I’m not expecting IsPromise to ever be exposed, ofc)
Why not? I would very much like to figure out a way to expose it
00:50
<bakkot>
they should not have to learn the difference
00:50
<bakkot>
that's my point
00:50
<Kris Kowal>
I do agree that we should generally steer folks away from checking whether things are promises.
00:50
<bakkot>
the thing I presented them as wanting to do was not "reject bluebird promises"
00:51
<ljharb>
i disagree with that opinion; Promise landed and other thenables are thus undesirable
00:51
<bakkot>
that is a totally unrelated thing to what I was hypothesizing
00:51
<Jessidhia>
my argument against it is an argument against the proposed use of the predicate — conditional await is misguided, leads to unpredictable execution order (“Zalgo 2.0”), and if you’re already in a context where you can await you are already asynchronous and there is no benefit to skipping awaits other than skipping the cooperative microtask yield point
00:51
<Kris Kowal>
I also agree that we should discourage conditionally awaiting.
00:52
<bakkot>
ljharb: it may well be that other thenables are undesirable but I don't understand what that has to do with the user story I'm discussing
00:52
<bakkot>
other thenables exist
00:52
<bakkot>
code which does if (typeof arg.then === 'function') arg = await arg; correctly interoperates with those thenables
00:52
<bakkot>
code which does if (Promise.isPromise(arg)) arg = await arg would not
00:52
<Kris Kowal>
I believe you’re also arguing that folks will inevitably abuse Promise.isPromise(x) in all these ways if a function by that name exists.
00:52
<bakkot>
yeah
00:53
<bakkot>
which is a claim I stand by
00:53
<devsnek>
i can see the dev.to articles now
00:53
<Kris Kowal>
I’m not sure that’s true. Q.isPromise has existed forever.
00:53
<ljharb>
a conditional await, i agree. A conditional validation exception tho is fine
00:53
<devsnek>
prescribing poor usage of IsPromise
00:53
<bakkot>
Kris Kowal: if you're using Q you are aware that there are multiple kinds of promise in the world and want to check specifically for Q's promises
00:53
<bakkot>
if you are a normal JS user you probably are not and should not have to be aware that other people are using other Promise implementations
00:54
<bakkot>
so it would not occur to you that Promise.isPromise is not the check you actually want
00:54
<ljharb>
imo they shouldn’t be using those other impls :-)
00:54
<bakkot>
your opinion is noted but I don't see what relevance it has
00:55
<snek>
what if Promise.isPromise(v) returns typeof v.then === 'function'
00:55
<bakkot>
there is, empirically, plenty of other code which is using other impls
00:55
<ljharb>
agreed
00:58
<Kris Kowal>
what if Promise.isPromise(v) returns typeof v.then === 'function'
typeof v.then === 'function' doesn’t need a name.
00:58
<Kris Kowal>
The motivation for a brand check is specifically to avoid reentrance from the API of the given value.
01:00
<snek>
i mean if you wanna just skip awaiting it
01:00
<snek>
that seems like anti-promise behavior
01:00
<snek>
the point of promises is to avoid zalgo logic right
01:00
<bakkot>
await invokes getters synchronously, is the concern discussed above
01:01
<snek>
yeah i saw that in the gh thread
01:03
<snek>
i don't quite understand what the concern was with that
01:03
<snek>
like "security" was the term used but that seems a bit excessive
01:04
<bakkot>
some people are writing code with very strong guarantees
01:52
<Mathieu Hofman>
Sorry I had to step away for a minute. Yes the concern is reentrancy, and knowing where that can happen or not. It's not obvious that doing Promise.resolve(x) can trigger synchronous execution of non platform code. And there is no way to prevent it without taking a tick hit for any non-primitive (which is what we're doing now)
16:37
<Mathieu Hofman>
So crazy suggestion, but what if we changed Promise.prototype [ @@toStringTag ] to do a IsPromise( this ) before returning "Promise" ?
16:38
<Mathieu Hofman>
Um we'd have to make it a get
16:38
<Mathieu Hofman>
yeah that would probably break something
16:57
<Kris Kowal>
Is Promise.isNativePromise more or less palatable?
17:02
<Jack Works>
Is Promise.isNativePromise more or less palatable?
Isn't that "real brand check" blocks SES?
17:26
<nicolo-ribaudo>
Sorry I had to step away for a minute. Yes the concern is reentrancy, and knowing where that can happen or not. It's not obvious that doing Promise.resolve(x) can trigger synchronous execution of non platform code. And there is no way to prevent it without taking a tick hit for any non-primitive (which is what we're doing now)

Unless I misunderstood what you mean, it can already trigger synchronous code execution:

let sync = true;
Promise.resolve({
  get then() {
    console.log("sync?", sync)
  }
});
sync = false;

logs sync? true

18:06
<Mathieu Hofman>
Yes, the goal is to be able to detect whether something is a native promise without triggering synchronous code execution. So that a "cast" would basically do const cast = (val) => (typeof val !== 'object' && typeof val !== 'function') || !val || isSafePromise(val) ? Promise.resolve(val) : Promise.resolve().then(() => val)
19:05
<Kris Kowal>
We discussed promise brand check in the SES meeting today and the good news is that we agree with everyone. (should be processed shortly) https://youtu.be/kLuJZiPUI44
19:06
<Kris Kowal>
In short, we want a brand check and we also would prefer it to be sufficiently obscure that people wouldn’t be tempted to use it to make a Zalgo induced fast lane for non-thenable native promises.
19:07
<Kris Kowal>
We already use a sufficiently obscure brand check for TypedArrays in our SES / hardened JavaScript / Lockdown shim, through the TypedArray prototype Symbol.toStringTag getter.
19:08
<Kris Kowal>
So we’re currently pondering whether we could navigate into a world where the same trick could be used for Promise.
19:11
<Kris Kowal>
I know @ljharb uses the same heinous crime in shims, though no such crime is yet possible with Promise. I assume having any way to brand check would be satisfying, even if that heinous.
19:36
<Kris Kowal>
Mathieu Hofman points out that the toStringTag getter trick would break the SES shim and probably other similar programs, so changing the property from value to getter wouldn’t be web compatible. I’m hoping we can come up with a similar trick. Something sufficiently obscure that people wouldn’t be tempted to misconstrue its purpose.
19:38
<ljharb>
So crazy suggestion, but what if we changed Promise.prototype [ @@toStringTag ] to do a IsPromise( this ) before returning "Promise" ?
that’s what every toStringTag should be changed to imo, a brand-checking getter that returns a string. If I’d thought of it before ES6 we could have done it then :-(
19:41
<Mathieu Hofman>
Besides the SES shim, I'm wondering what else would break if we did this
19:43
<Mathieu Hofman>
The SES shim refuses to initialize if it finds a getter instead of a string value, at least it "fails safe", but unless people update, it's definitely a breaking change.
20:09
<ljharb>
the SES shim could be changed tho to just replace the getter, right?
20:09
<ljharb>
that seems acceptable for this level of win. But that presumes it’d be an acceptable change to the wider committee
20:10
<Mathieu Hofman>
The SES shim can be updated of course, but there are people using it out there that would need to update to unbreak
20:10
<bakkot>
what's the proposal for the behavior when IsPromise(this) returns false? throwing seems bad
20:11
<bakkot>
but no other option seems obvious to me
20:11
<Mathieu Hofman>
return undefined is what TypedArray does
20:11
<bakkot>
hm, ok
20:11
<bakkot>
did not know TA did that
20:12
<bakkot>
oh, because of the shared prototype, right
20:13
<Mathieu Hofman>
yeah it causes Object.prototype.toString() to fallback to the builtinTag aka "Object"
20:14
<ljharb>
definitely any such getter wouldn't need to throw if it returned a different string when the brand was absent
20:47
<bakkot>
Mathieu Hofman: my main takeaway from watching the SES meeting linked above is that not enough people know about the Pin functionality in the spec document
20:47
<bakkot>
can recommend: pin things you keep coming back to
20:57
<Mathieu Hofman>
Genius, how did I not know about this!
21:05
<bakkot>
anyway: I am ok with making Promise.prototye[Symbol.toStringTag] be a getter which does if (IsPromise(this)) return "Promise"; return undefined;. that seems sufficiently obscure that people will only use it in obscure cases, as long as ljharb agrees not to publish an is-promise package on npm
21:06
<bakkot>
(fortunately the actual is-promise on npm is just the thenable check)
21:11
<ljharb>
lol i mean, i already have IsPromise in es-abstract and it'd definitely use this
21:11
<ljharb>
but there wouldn't be much of a need for a separate package
21:31
<Ashley Claymore>
Yes, the goal is to be able to detect whether something is a native promise without triggering synchronous code execution. So that a "cast" would basically do const cast = (val) => (typeof val !== 'object' && typeof val !== 'function') || !val || isSafePromise(val) ? Promise.resolve(val) : Promise.resolve().then(() => val)
Wouldn’t a custom subclass of promise pass the brand check but still trigger sync user code if it overrrides then?
21:33
<Mathieu Hofman>
const isSafePromise = (p) => Promise.isPromise(p) && !Reflect.isExtensible(p) && Reflect.getPrototypeOf(p) === Promise.prototype && !Reflect.getOwnPropertyDescriptor(p, 'then') && !Reflect.getOwnPropertyDescriptor(p, 'constructor') (Given frozen instrinsics)
Ashley Claymore: isSafePromise would have to check for the proto
23:59
<Justin Ridgewell>
I don't understand how a "is safe promise" won't immediately be misused.