Updated: I've tried to provide a better context for this discussion with my new post: "Thenable: how to make a JavaScript object await-friendly, and why it is useful".
Asking for opinions. Would it make sense to have a standard symbol for the object's default awaitable, e.g. Symbol.promise
, by analog to Symbol.asyncIterator
?
I sometimes use the following pattern, in a nutshell (codepen):
class AsyncOperation {
#promise = null;
constructor(ms) {
this.#promise = new Promise(r => setTimeout(r, ms));
}
then(resolve, reject) {
return this.#promise.then(resolve, reject); }
}
async function main() {
await new AsyncOperation(1000);
console.log("completed!")
}
This works, because we've made an instance AsyncOperation
to be thenable
.
If however we had Symbol.promise
, it'd be less boilerplate code for AsyncOperation
:
class AsyncOperation {
#promise = null;
constructor(ms) {
this.#promise = new Promise(r => setTimeout(r, ms));
}
get [Symbol.promise]() { return this.#promise; }
}
Wouldn't Symbol.promise
be useful?
Here is a less contrived snippet, adapted from some real-life code of mine:
const subscription = createSubscription(
cancellationToken, eventSource, "eventName");
try {
await subscription;
}
finally {
subscription.close();
}
The relevant part of createSubscription
code:
// ...
return Object.freeze({
close: () => unsubscribe(),
then: (resolve, reject) => promise.then(resolve, reject)
});
I'd like to be able to do:
// ...
return Object.freeze({
close: () => unsubscribe(),
get [Symbol.promise]() { return promise; }
});
Of course, I could as well just expose promise
as a property getter (and do await subscription.promise
), or as a method, similar to RxJS' toPromise()
.
Yet the same arguments could possibly be used for iterable objects, which nevertheless expose their iterators via [Symbol.iterator
] or [Symbol.asyncIterator
]. Not via something like array.iterator
or array.getIterator()
.
IMO, it'd be convenient if await
looked for [Symbol.promise]
in the same way the for...of
and for await...of
loops look for iterators.
Given that we already can await
any expression in JavaScript (not just a Promise
or "thenable"), I think that would make sense.
Top comments (8)
Probably, the common JavaScript way of calling an async operation is less OOP-ish:
I might image that you want an object to get a
cancel
method or aprogress
property, which isn't standard by itself. There's a Stage 1 proposal for Cancellation API, but who knows how it will look like.Looking into Subscription code, if a promise is one-time only, why doesn't it close itself on finish? Then it will be just
The
close
method on subscription might be useful for scenarios like this:In which case, if
anotherPromise
wins the race, I want to synchronously stopsubscription
.As to cancellation, I currently use Prex library, for its close resemblance with .NET cancellation framework. I mentioned that in the TC39 cancellation discussion thread. Indeed, it's hard to predict what the final standard will be and it may take years before it reaches stage 4, so I'm just using something that is available today.
If your
close
is mycancel
and cancelling an already settled Promise makes no harm:But with finalized Cancellation API it could be something completely different.
I also could give you another idea for fun:
Now it's officially a Promise without needing a global Symbol ))
I like your ideas, but the thing is, in the lacks of the standard cancellation framework for JavaScript, we all use different libraries. So, what works for you might not work for me :)
The prior art behind the current TC39 cancellation proposal is the .NET cancellation model, and it's also what's behind Prex, the library I use. It has two separate but connected concepts: CancellationTokenSource (the producer side of the API) and CancellationToken (the consumer part of it). Cancellation can be initiated on the source only, and observed on the token.
That makes sense, because cancellation is external to an API. But this way, using cancellation for stopping subscriptions gets a bit bulky, because I now need to create a temporary, linked token source just for the scope of my subscription, only to be able to cancel it (here is the complete runkit):
Personally, I'd rather stick to using the
subscription.close
pattern, which is less verbose and reduces the number of allocations:If there was standard
Symbol.promise
, it'd bePromise.race([s[Symbol.promise], p])
, which I still think isn't too bad.Your mileage with this may vary, depending on the libraries you use.
Prex has Deferred class for that, and I use it a lot :) It's semantically close to .NET TaskCompletionSource.
Thanks for the discussion!
Are you aware of the binding proposal?
That might reduce your boilerplate a little:
And:
If the proposal was accepted, would you still push for
Symbol.promise
?I actually wasn't aware of the binding proposal, thanks for pointing it out! It'd be great if it makes to the final stage soon.
I still think
Symbol.promise
would make sense ifawait
was aware of it, to save a few implicit allocations otherwise incurred by awaiting viaobj.then
. Also, having a direct access to the object's default promise might save me from doing something like this:new Promise((...a) => obj.then(...a))
, whenthen
isn't enough and I need a promise - e.g., for use withPromise.race()
.It'd also help to if I need the promise itself
I don't know enough about the mechanics and overhead of
await
to comment on that.In the
Promise.race
case, it seems to handle primitives and thenables directly, so there may not be any need to wrap as a 'real promise'.Reading up on
Promise.resolve
- is there a reason you can't usePromise.resolve(thenable)
rather than spreading args with yournew Promise
idiom?I find the Promise part of the EcmaScript spec really hard to read, so refer to the comments against Promise.resolve on MDN:
It never occurred to me I could use a
thenable
withPromise.resolve
andPromise.race
etc. Today I've learnt something new, thanks to you! It really does work:I could recommend this read on V8.dev: Faster async functions and promises.