Html: A setTimeout that returns a promise that resolves after specified time.

Created on 3 Feb 2016  Â·  55Comments  Â·  Source: whatwg/html

I really love Promise.resolve("some value"), however if you want to delay this resolving you have to do something much uglier like

new Promise(resolve => setTimeout(() => resolve("some value"), 1000));

Would love a way to create a promise that waits and resolves. I made a proposal for Promise.after to ES and was informed it is a better fit to recommend here.

It seems a global function might make more sense in this context. Maybe something like resolveIn(1000) that would return a promise that resolves in 1000 milliseconds. This could even be implemented like so:

function resolveIn(milliseconds) {
    return new Promise(resolve => setTimeout(() => resolve(milliseconds), milliseconds));
}

And could be used like:

resolveIn(1000).then(/* do something */);

And could be used to resolve a specified value after a delay like so:

resolveIn(1000).then(() => "some value").then(/* do something with "some value" */);
additioproposal needs implementer interest

Most helpful comment

@Jack-Works

They can throw (based on spec text of my proposal). But whatever, I'm not going to push this to stage 1 now.

I am not trying to convince you to _not_ pursue this. Please don't take my/our comments as attacks or discouraging.

You are looking to contribute in a (pretty large) area that some of us care about (Some examples: Domenic and Jordan from the specification and whatwg point of view, myself and Jordan from Node timers and I also help maintain sinon/jest's fake-timers).

People are just indicating that TC39 is not the correct avenue to push this because of the scope of what's part of ECMAScript and what's part of the platform.

Domenic's first ever comment to you here was to suggest you in fact work on this :]

Here are some constructive ways to contribute to this (feel free to do some, or none at all):

  • Play with Node's experimental timers/promises to find pros/cons for that sort of API to help bring it as prior art to whatwg for timers.
  • Add tests for timers/promises in Node.
  • Go over the timers specification and get familiar with the machinery used there (like the spec language, terms and bikeshed). Find bits that are not covered by the wpt and add tests for it.

All 55 comments

I am in support of this idea (with a better name, but we can bikeshed that later). I would like to hear if any implementers would implement this assuming I specced it.

How about cancelation? clearResolveIn(promise)? Cancelable promises?

I think we would have to wait for cancelable promises (in whatever form they show up in). That might be enough to sink this feature until that work is done, but I personally don't think it's necessary---adding it right now, and making it cancelable later, would be OK in my book.

Are we absolutely sure that making this cancelable later won't be a breaking change or introduce nasty duplication like cancelableResolveIn() or CancelablePromise.after()?

Without cancelation, this proposal is just a shortcut to a one-liner which we can (and do) live without. Achieving clearTimeout functionality with promises is a lot harder and it would be better if this proposal could solve that issue.

Are we absolutely sure that making this cancelable later won't be a breaking change or introduce nasty duplication like cancelableResolveIn() or CancelablePromise.after()?

If promises become cancelable in the future, this would automatically become cancelable too.

it would be better if this proposal could solve that issue.

These are 2 separate issues.

Are we absolutely sure that making this cancelable later won't be a breaking change

Yes, sort of. From one perspective, it could be a breaking change, since code that previously assumed a promise would never be canceled would suddenly start to have to deal with that. This is a general problem with cancelable promises across the platform and is part of the reason why I am scared to start working on them because it will start causing lots of debates. (Which I really do not want to reiterate in this thread; so let's try to drop this line of discussion.)

or introduce nasty duplication like cancelableResolveIn() or CancelablePromise.after()?

We can guarantee this will not happen, yes.

Without cancelation, this proposal is just a shortcut to a one-liner which we can (and do) live without.

Even with cancelation, your statement applies. This proposal is entirely about convenience.

Are we absolutely sure that making this cancelable later won't be a breaking change

Yes, sort of.

Cool. :+1:

Would TC39 not want this to be a static on Promise?

Would TC39 not want this to be a static on Promise?

I am pretty sure no. TC39 does not define an event loop or a notion of time; that is for the embedder. And using statics as a place to store utility methods is not a great pattern; it's forced by necessity in many cases, but in general statics should be reserved for things that vary among subclasses. You could argue this is one such thing, but I am not sure myself...

wait Doesn't look to be a reserved word and I think wait(500).then(x) reads rather well.

Hey, copying my reply from the duplicate (thanks for finding this Domenic):

wanderview: I'm somewhat ambivalent, but any such API probably also needs something equivalent to clearTimeout(). Maybe using AbortSignal/AbortController.

Sure, what if we allow passing an AbortController.signal as an optional second parameter?

await delay(3000, { signal: controller.signal} );

In a way similar to fetch?

Another interesting idea is a variant of setInterval that returns an async iterator, though we might not want to tie the two together.

for await(const time of interval(1000)) {
 // do something, `break` here to cancel (or `.return` on the iterator)
}

AbortController seems like a very odd way around the issue here. I've never heard of the api before but I am not fond of it so far.

This is because it tries to circle around access to the object itself, because Promises are supposed to be immutable and have no direct object access, because apparently people want to return just a Promise.

So here we have something persistent. Particularly in the case of intervals, but also in the case of a Timeout -- for example, in Node timeouts may be 'refed' and 'unrefed' from the event loop, refreshed (as the same object/id), and of course you may simply just want to cancel the timeout (which happens constantly if you are implementing any network protocol).

Another angle, brought up by @jasnell: For an interval, one might want to consume it as an async iterator, something like this:

async foo() {
  const interval = setInterval(fn, 1000)
  for await (const m of interval) {
    /* ... */
    clearInterval(interval) // would end the iterator...
  }
)

Promises do not modal persistently intractable interfaces. That is the problem. _(Not just, not well... they just don't at all.)_

Cancellable promises only help one very specific part of this issue, it should be noted.

This problem isn't just isolated to timeouts either, there a lot of APIs in Node where we'd like to "have a promise version" (really "an awaitable version") but we also need to return immediate access to something with... events and handlers and stuff. Like a child process or http server.

So, to model this "correctly" by allowing direct access the persistent objects in question, I have suggested that Node make timers "thenable". Which I am moving on recommending for other apis in node as well (as noted in this twitter thread about the subject (please don't reply to that though)). It's "ugly" but the other options are... well, there isn't much in terms of options given the constraints.

I don't have a good answer for web browsers which for some forlorn reason return a _number_ from setTimeout(). _(Node.js can't and won't stop returning an object, if you are wondering... as I am sure is the same for the dom and numbers)_

AbortController is the web platform's primitive for cancelation, so that is what we will use for the promise-returning setTimeout analog. I can't speak to the larger issues in your message; I'm unsure how they're related to this issue (or to the web platform).

I can't speak to the larger issues in your message; I'm unsure how they're related to this issue (or to the web platform).

They are related because (as sizeable group) folks want web compatibility and want to work with e.g. the whatwg to come up with things that work outside web platforms.

Node is also encountering this issue of "people want an awaitable timeout", but also that has the related issues noted above.

That AbortController/AbortSignal unifies cancellation is its most important property. Right now people are mainly familiar with AC because of fetch, but the key to its effective use is to make all cancellable APIs respect it. This allows one signal to correspond to one cancellable ‘transaction’.

AC is kinda ugly to work with directly, but this is in part because cancellation is an ugly problem. I’d still rather see Node implement AbortController (as they’ve implemented URL, TextEncoder, etc) than see HTML not include it here.

@Fishrock123 you and @domenic are not actually disagreeing on this from what I understand. You can have setInterval as an async iterator and at the same time have a cancellable timeout with AbortController.

I started to strawman this out a bit: https://gist.github.com/devsnek/9686da77db0421927c84e862bd70b214

one of my main goals with this approach is to sort of streamline everything into one neat api that's small so its easy to remember and use.

One of my assumptions for this is that https://github.com/tc39/proposal-cancellation moves forward, although it's not an explicit requirement.

FYI the delay package on npm supports cancellation with AbortSignal: https://www.npmjs.com/package/delay#signal

@Jack-Works as discussed previously in this thread, such a proposal is not appropriate for the JavaScript language, but instead needs to be part of the web platform.

I didn't invent the notion of time, I leave it for the host to interpreter what does the time parameter means.

I also didn't add something like "event loop" to the langauge, I'm re-using the existing Job concept for scheduling task.

Job is not appropriate for this. Also the lack of integration with AbortSignal is a killer.

Do you have something in particular against doing this in the correct standards body?

It seems like no one is working on this in whatwg for years.

And I don't like it appears on the AbortController. If so, how can I use it in Node? If it is not in the language, not every platform will implement it.

It seems like no one is working on this in whatwg for years.

Yes. So why don't you?

If so, how can I use it in Node?

Node.js has AbortController: https://github.com/nodejs/node/issues/31971#issuecomment-646333931

it is not in the language, not every platform will implement it.

That's not true. Many features standardized outside of TC39 are implemented on multiple platforms. For example, the Encoding Standard, performance.now(), the URL Standard, ...

Yes. So why don't you?

I'm not familiar with the workflow of whatwg, I assume I need to find a champion to support the idea (tell me if I'm wrong) and I asked for someone I familiar with working in a w3c member company (not whatwg) and he also didn't have interest (or be able) to help me. I was posted it on the WICG discourse before, and it seems the same. I guess the community is not very enthusiastic about this topic.

That's not true. Many features standardized outside of TC39 are implemented on multiple platforms. For example, the Encoding Standard, performance.now(), the URL Standard, ...

It's non-mandatory, and platforms may have inconsistent APIs. Sometimes I write library only use things defined in the ES spec to make sure it _will_ work on every engine. I even cannot use console to log anything or a "timer" cause all of those are not in the language. The language is lacking too many things in it. Promise.delay could be a modern standardized version of setTimeout.

You can learn more about the WHATWG working mode on our website: https://whatwg.org/working-mode. Anyone can contribute; there is no need for a champion. The W3C is a separate organization, and their member company structure is not related to the WHATWG.

There is indeed a looming question over all of this, as to whether any browser companies/JS engine companies (they are the same) are willing to invest time implementing this. For Chrome/V8, I can say that we're not interested in any version without AbortSignal integration, or any version that does not use the same specification infrastructure as setTimeout. But even if those requirements are met, whether we can devote staffing to such an API is unclear; slightly-more-convenient versions of things you can do already are often a hard sell. A good first step would be a pull request to the HTML Standard and the web platform tests project, though.

Another question, why can't we have both? (Promise based API and also modify the AbortController) They're different things(one based on Promise one based on event) so we can add timeout support for them and that doesn't constitute the duplication.

Promise static methods need to be related to promises, not to scheduling and time.

To be clear, the intention is not to modify AbortController. It's to add a new global method (or perhaps a new method to the Scheduler class) which accepts an AbortSignal and returns a promise.

Two promise-returning functions that settle after a given time is duplicative.

timer improvements seems like something that could be part of the task api, unless that's too far along at this point or too narrowly scoped? nvm you had the same idea :P

Hmm, actually, refreshing my memory on the scheduling API I linked to above, it looks like we are already implementing this in Chrome! The most up-to-date explainer I found was at https://github.com/WICG/main-thread-scheduling/blob/master/PrioritizedPostTask.md#posting-delayed-tasks

const promise = scheduler.postTask(fn, { delay: 100, signal });

Maybe this issue can be closed then :)

hmm cool api, I never learned that. but that API seems very Web specific.
Another question is, does Promise.timeout case resolved?

I don't think there's much web-specific about that API.

I don't understand your second sentence.

Hmm I'll learn the api later thanks

const promise = scheduler.postTask(() => {}, { delay: 100 });

Okay I have tried this. This does give me a promise that resolved after 100ms. But if I want to pause the async function for certain time, this API doesn't match the semantics I expected.

By await Promise.delay(100), the code meaning "wait for a Promise that resolves after 100ms".

But await scheduler.postTask(() => {}, { delay: 100 }) means "schedule an empty task that run after 100ms, and wait for the task done". This doesn't seem right on the coding semantics. It is "I want to do something but there is no API, but Ah-Ha, I know there is a Web API that accidently have the same result I want so let's use it". It's a code smell to me.

They're different things. Use scheduler.postTask as a sleep function is using it with wrong intension.

Another question is, how to make the same result as Promise.timeout(promise, time) (Return a new promise that if promise is still pending after time, reject this new Promise) using scheduler API?

That's not true. Many features standardized outside of TC39 are implemented on multiple platforms. For example, the Encoding Standard, performance.now(), the URL Standard, ...

Well for this case, I found scheduler is not in the NodeJS global nor Deno global. It means if I use scheduler to delay or timeout, my code is not work for both platforms.

They're not different things. In fact, "a Promise that fulfills after 100 ms" can only be accomplished by posting a task to a scheduler. That's why it's not acceptable to do this via the language. The API presented above is more honest than Promise.delay, which is pretending that something scheduler-related is promise-related.

That's why I said above "Promise static methods need to be related to promises, not scheduling and time".

Scheduler is not yet implemented in Node. Nor in Firefox. Nor in stable Chrome. But all those things will come in time.

well if the priority names continue to be stuff like "user-blocking" it probably won't come to node but eh someone will probably make a polyfill

Node has users...

sure, but we don't have a prioritized event loop like browsers, it's just "run everything asap".

That's surprising. You could ignore the priority signal if you don't think it's useful, but you can also use it as signal to better scheduling decisions.

In fact, "a Promise that fulfills after 100 ms" can _only_ be accomplished by posting a task to a scheduler. That's _why_ it's not acceptable to do this via the language. 

It's not true. In 8.4.4 HostEnqueuePromiseJob it says:

HostEnqueuePromiseJob is a host-defined abstract operation that schedules the Job Abstract Closure job to be performed, at some future time.

It does mention future time. I just want to add a hint to the host that it should wait for how much time before the _future time_.

That's not what that section means. (I can tell you, having written it.)

It refers to a host's ability to chronologically order events, not a host's ability to tell time.

For what it's worth I brought task scheduling to Node once and people weren't very interested in that API.

That said Node already ships (experimentally, and not on a global) promise based versions of setTimeout (sorry if that was previously mentioned and I missed it):

'use strict';

const {setTimeout} = require('timers/promises');

(async () => { 
  await setTimeout(100); // wait 100 ms
  const controller = new AbortController();
  queueMicrotask(() => controller.abort()); 
  await setTimeout(100, { signal: controller.signal });
})();

Note that this is:
a) Experimental
b) Different from a timer specification point of view. Node's timers aren't web timers for starters - they return objects.

So whatever Node does here should not necessarily influence what browsers do.

(to be clear, timers/promises will ship in node 15, and has not actually shipped yet)

So Node need "require(...)" to use the promised timer. That also not good for cross-platform code.

@domenic 's argument only works if major platforms implement it in exact same API (a global object). If they need different way to access, "are implemented in multiple platforms" doesn't provide convenience for this simple function. Hand written one is much easy than environment detect and load different native apis.

@Jack-Works it can be imported as well in node.

So Node need "require(...)" to use the promised timer. That also not good for cross-platform code.

What cross-platform code? Timers are not and never were cross-platform. Node does not implement the web-timers specification and while it follows some quirks it doesn't follow others. Additionally it has its own quirks that likely go against the spec.

Node does make timers interop nicely in universal code when possible - for example the new toPrimitive.

Node does have some spec compliant APIs (like URL) and there it follows the same spec as browsers.

What cross-platform code? Timers are not and never were cross-platform.

That's why we need one.

What about environments that run JavaScript but have no concept of timers like certain embedded environments?

What about environments that run JavaScript but have no concept of timers like certain embedded environments?

They can throw (based on spec text of my proposal). But whatever, I'm not going to push this to stage 1 now.

@Jack-Works

They can throw (based on spec text of my proposal). But whatever, I'm not going to push this to stage 1 now.

I am not trying to convince you to _not_ pursue this. Please don't take my/our comments as attacks or discouraging.

You are looking to contribute in a (pretty large) area that some of us care about (Some examples: Domenic and Jordan from the specification and whatwg point of view, myself and Jordan from Node timers and I also help maintain sinon/jest's fake-timers).

People are just indicating that TC39 is not the correct avenue to push this because of the scope of what's part of ECMAScript and what's part of the platform.

Domenic's first ever comment to you here was to suggest you in fact work on this :]

Here are some constructive ways to contribute to this (feel free to do some, or none at all):

  • Play with Node's experimental timers/promises to find pros/cons for that sort of API to help bring it as prior art to whatwg for timers.
  • Add tests for timers/promises in Node.
  • Go over the timers specification and get familiar with the machinery used there (like the spec language, terms and bikeshed). Find bits that are not covered by the wpt and add tests for it.

I am not trying to convince you to not pursue this. Please don't take my/our comments as attacks or discouraging.

Thanks I will ✨

@domenic says

For Chrome/V8, I can say that we're not interested in any version without AbortSignal integration, or any version that does not use the same specification infrastructure as setTimeout.

With objections from implementor, it's no meaning to push this forward because it will be blocked earlier or later. (And therefore it's meaningless to present to stage 1)
My main purpose is to suggest a unified API but it becomes impossible.

By accepting this irreversible conclusion, I'm OK to write platform-aware code.

Since I have to write a helper function whatever I'm not so motivated in investigating timer API in any major platform cause it already has the ability to implement what I want in the user land.

I think the Chrome/V8 requests Domenic raised (AbortSignal integration since that's the built-in cancellation mechanism, and reusing the same timer infrastructure) are pretty reasonable and I would (unsafely) assume other implementors would have similar requirements in order to avoid further fragmentation.

That said such an API (With an optional AbortSignal) would address both your use case (of promise returning timers) and Chrome's (reusing existing machinery). In fact that's the approach Node took.

This is entirely possible and probably (hopefully) we'll eventually get this sort of API.

The thing is: doing that as a first contribution to whatwg/html sounds like a very tall order to me which is why I suggested API feedback / adding tests to the existing machinery.

I didn't know there's a discussion about this going on (I was looking for similar topics in ES proposals). I've prepared an explainer for the same idea. The solution is different though. Maybe it would be helpful in this discussion: https://github.com/jarrodek/delayed-promises

FYI, Node.js 15 shipped timers/promises module. https://github.com/nodejs/node/pull/33950 (Edit: as https://github.com/whatwg/html/issues/617#issuecomment-703134644 said)

Edit 2: I'm trying writing a PR.

Was this page helpful?
0 / 5 - 0 ratings