Promise-returning delay function

# Domenic Denicola (10 years ago)

Spinning off a new thread...

From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Mark S. Miller Subject: RE: "use strict" VS setTimeout

Let's just be sure that we avoid this mistake when promises grow something like Q's Q.delay. Promise.delay? Promise.prototype.delay?

IMO this should be a utility function that uses promises, not a feature of promises/promise classes themselves. So it would be e.g. global.delay or import delay from "js/flow" or something. I think in general promise utility functions are a slippery slope and I'd feel more comfortable sliding down that slope if we kept them in a module instead of grafting them onto the class itself. I'd rather reserve that for things that are core to the promise experience, either control-flow wise (Promise.prototype.finally, perhaps Promise.any) or for pipelining (.get, .call, etc.).

BTW I definitely agree that promise-returning delay(ms) as a better setTimeout should be part of the standard library.

(I also find the name sleep somewhat attractive, as a re-appropriation from threaded contexts. await sleep(1000), heh.)

# Sébastien Cevey (10 years ago)

On 9 Sep 2014 00:13, "Domenic Denicola" <domenic at domenicdenicola.com> wrote:

(I also find the name sleep somewhat attractive, as a re-appropriation

from threaded contexts. await sleep(1000), heh.)

Sleep has blocking semantics in all languages I've used it in (eg. C, Java, Ruby, Bash, etc) so perhaps best avoid any confusion?

I've found delay very useful and natural in the context of Rx.Observables, where it is a method to delay each element in a stream, although that's a slightly different use case to what's proposed here.

What's the recommended way to add capabilities to Promises? Utility functions, as suggested here? Promise subclassing/"casting"? Prototype monkey patching (I guess not, but some will do that if there's no better way...)?

Reminds me from the elegant/surprising obj::func() binding syntax Domenic showed the other day in a presentation, too.

# C. Scott Ananian (10 years ago)

The npm prfun package uses both prototypes patching and subclassing, fwiw. And it implements both Promise.delay and Promise.prototype.delay. I'd be interested in feedback after folks use it a bit.

# Brendan Eich (9 years ago)

Domenic Denicola wrote:

BTW I definitely agree that promise-returning delay(ms) as a better setTimeout should be part of the standard library.

See twitter.com/garybernhardt/status/526435690524196864 where I'm subtweeted but with some justice. setTimeout began life taking only a program to eval, and given the general implicit-conversion mania afflicting me and others at Netscape, it converts its first argument to string source for that program. Only in Netscape 3 did the funarg-first form come in.

Sorry about that, and you're right -- at some promise-allocation and cognitive load costs, a new API could win. Another option is to have a new functional API, but I bet it won't be easy to name well or get approved.

# Andrea Giammarchi (9 years ago)

Not sure if discussed already but I believe .cancel() or .forgetAboutIt() mechanism should be proposed before introducing any .delay()

Promises often put developers in unmanaged roads where things keep being "forgotten" and/or impossible to drop without ending up in the next chain, undesired, or the error stack, even less desired.

What I mean is that if setTimeout is a desired pattern to replicate through .delay(ms), but I really don't understand why that would be a desired pattern at all, clearTimeout functionality should be considered as well.

A generic .cancel() would be most likely the easier approach but it should be implemented upfront.

# Tab Atkins Jr. (9 years ago)

On Mon, Oct 27, 2014 at 3:21 AM, Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:

Not sure if discussed already but I believe .cancel() or .forgetAboutIt() mechanism should be proposed before introducing any .delay()

Promises often put developers in unmanaged roads where things keep being "forgotten" and/or impossible to drop without ending up in the next chain, undesired, or the error stack, even less desired.

What I mean is that if setTimeout is a desired pattern to replicate through .delay(ms), but I really don't understand why that would be a desired pattern at all, clearTimeout functionality should be considered as well.

A generic .cancel() would be most likely the easier approach but it should be implemented upfront.

Cancelable promises are a completely different beast; it's not just a matter of adding a new method to Promise.prototype, it's changing the data structure pretty fundamentally. Suddenly you have a new value-affecting capability, but it's exposed outside of the constructor. You need to be able to react to a cancellation, so you can actually cancel the work being done, and possibly resolve the promise to some default value (or error).

A promise that auto-resolves after a timeout, on the other hand, is a straightforward utility function of obvious usefulness, and requires no additional work:

Promise.delay = function(ms) { return new Promise(function(r) { setTimeout(r, ms); }); }

(Not tested, but this should be approximately right.)

# Andrea Giammarchi (9 years ago)

I thought promises were introduced to simply the "whenever it happens" pattern ... using a delay seems a pretty pointless feature to me and setTimeout worked so far (15 years?) pretty well and it is cancelable.

Regardless delay though, canceling, beast or not, should be considered before all these changes will make it "impossible" to implement, IMO

# C. Scott Ananian (9 years ago)

There's already a way to cancel a Promise -- use its reject function. eg.

var reject;
Promise.delay(100/*ms*/).then(function() { reject(new Error('took too
long')); });
return new Promise(function(resolve, _reject) {
  reject = _reject;
  resolve(doSomethingTimeConsuming());
});

There's no need to rethink or redesign anything.

See cscott/prfun#promisetimeoutint-ms--string-message--promise for a nice example, borrowed from bluebird.

# Andrea Giammarchi (9 years ago)

canceling should not pass through any errorback chain... Canceling is like an explicit abort on xhr ... Not an error, not a fulfillment, it's a third state: cancelled

# C. Scott Ananian (9 years ago)

So throw a different class of Error, or resolve with a different type of object. Can you substantiate your assertion better?

In my experience, regularity of control flow constructs is a feature, not a bug. I don't want to have to reason about some "third way" control flow.

# Andrea Giammarchi (9 years ago)

The moment you pass a promise you have no control on who's adding what as callback or errorback which means you have no control over a .reject() or over a .success()

.cancel() would eventually keep both queues unresolved so that nothing should happen at all: no error handling, no error happened, and no actions needed ... it's simply canceled.

This is the same reason .fetch() API needs also events to work properly (including progress) ... you "cannot" base as example typeahead on promises right now 'cause it's a mess to handle since response order is not guaranteed but you need to handle the .cancel() case without telling the user something else went wrong ... what you suggest, a new Noop() where Noop.prototype = Object.create(Error.prototype) ?

And how do you instruct unknown surrounding code that an if (err instanceof Noop) should be the first line of every errback ?

This is the reason few devs cannot adopt current standard Promises.

.cancel() as well as .abort() is very important when many network operations are in place and/or you want to switch to a different state without waiting for the previous one to be fulfilled.

new Promise(function (resolve, reject, cancel) {}); is the dumbest idea I could have now beside new Promise(function (resolve, reject) {}).on('cancel', cancelback).then(whatever) one

Anyway, this would deserve IMO a thread a part but I hope this topic will be discussed at some point.

Best

# Dean Landolt (9 years ago)

On Tue, Oct 28, 2014 at 10:41 AM, Andrea Giammarchi < andrea.giammarchi at gmail.com> wrote:

The moment you pass a promise you have no control on who's adding what as callback or errorback which means you have no control over a .reject() or over a .success()

.cancel() would eventually keep both queues unresolved so that nothing should happen at all: no error handling, no error happened, and no actions needed ... it's simply canceled.

This is the same reason .fetch() API needs also events to work properly (including progress) ... you "cannot" base as example typeahead on promises right now 'cause it's a mess to handle since response order is not guaranteed but you need to handle the .cancel() case without telling the user something else went wrong ... what you suggest, a new Noop() where Noop.prototype = Object.create(Error.prototype) ?

And how do you instruct unknown surrounding code that an if (err instanceof Noop) should be the first line of every errback ?

This is the reason few devs cannot adopt current standard Promises.

.cancel() as well as .abort() is very important when many network operations are in place and/or you want to switch to a different state without waiting for the previous one to be fulfilled.

new Promise(function (resolve, reject, cancel) {}); is the dumbest idea I could have now beside new Promise(function (resolve, reject) {}).on('cancel', cancelback).then(whatever) one

Anyway, this would deserve IMO a thread a part but I hope this topic will be discussed at some point.

Best

I agree w/ Scott re: regularity of flow control, and I'd be particularly uneasy if a cancel method on a promise had any impact on upstream handlers. But ISTM something like this could be made to work (even if it looks a bit silly):

var cancelled;
var never = new Promise(function () {});
Promise.delay(100/*ms*/).then(function(value) {
    return cancelled ? never : value;
}, function (error) {
    return cancelled ? never : throw error;
});
function cancel() { cancel = true };

This could be cleaned up into a cancellation helper to quash downstream promises (this kind of utility would be a good use case for an always method if one isn't yet spec'd).

Though I do wonder what happens to promises like these that get dropped on the floor? Will downstream handlers be GC'd? Is this specified in any way? I imagine dev tools want to warn about these kinds of promises — it may be helpful to have a way to signal that a promise was intentionally quashed.

# C. Scott Ananian (9 years ago)

You probably want streams for typeahead, not promises.

# C. Scott Ananian (9 years ago)

Dean, your idea is even better as a Promise subclass.

The subclassed cancellable promises could share a cancel flag, and throw an exception (or resolve to a different value, whichever you like better for the API) when the cancel flag is set. The then method on CancellablePromise returns another CancellablePromise (the spec is nicely written this way), and you use Promise.resolve to cast to a standard Promise to end the cancellation chain at an appropriate place.

See the Promise.bind implementation in prfun for an example of using subclasses in this way: cscott/prfun/blob/master/lib/index.js#L518 --scott

ps. Of course, to be precise, your API would be a factory function which makes a new subclass of CancellablePromise for each cancel scope, since the Promise spec associates the state with the constructor identity. (I had some small issues with the "new subclass for each X" mechanism in an earlier attempt at creating MonadicPromise using subclasses, but I think these were ultimately resolved in the spec---and in that case I was creating a new subclass for each call to Promise.resolve, which was a whole 'nuther kettle of fish.)

# Andrea Giammarchi (9 years ago)

I actually don't want Promises at all with network interactions but you have to explain this to all developers out there that think Promises are the Eldorado for everything asynchronous ... I found myself needing a cancel mechanism that goes beyond your outer scoped flag.

I really need to drop that action ... not to wait for it to fulfill and then do something else.

Imagine it's not about network, imagine it's "a lift" ... door are closing, someone passes in the middle, what do you do now: sound the alert after chopping the person that tried to enter in the meanwhile or change floor ... after chopping the person anyway ?

If promises could handle edge cases events too I think we could handle above situation instead of chopping that poor person.

I hope you got what I mean but again this most likely need another thread.

Best

# C. Scott Ananian (9 years ago)

On Tue, Oct 28, 2014 at 2:24 PM, Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:

I really need to drop that action ... not to wait for it to fulfill and then do something else.

This is orthogonal to the use of Promises, which just discuss control flow.

Imagine it's not about network, imagine it's "a lift" ... door are closing, someone passes in the middle, what do you do now: sound the alert after chopping the person that tried to enter in the meanwhile or change floor ... after chopping the person anyway ?

You call the Door.interrupt() API method to indicate that a person is present/cancel the opening.

Again, this is orthogonal to the Promise that the Door.close() method returned. If Door.close() returned a Promise, then that promise could then resolve to "cancelled" value (instead of "success"), or else you could decide that rejecting the promise with a DoorCloseInterrupted exception makes more sense.

The asynchronous cancellation mechanism is not related to Promises, and shouldn't be conflated with the Promise API. The Promise mechanism just directs control flow after the door has either been successfully closed or cancelled.

Concretely:

function tryToAscend() {
return Door.close().then(function() { return raiseLift(); },
function(e) { return Door.open().delay(10*1000).then(tryToAscend); });
};
# Andrea Giammarchi (9 years ago)

Again, you might have an error that comes from "ringTheAlarmBell()" API ... you are making the problem simple but simple is not the moment all you do is returning a Promise with binary result (success/error) without considering that when you reopen, much more could happen ... you want to branch out, not keeping doing the same action and that error could come for many reasons.

Your control flow here, if you have to take care of all cases, will become a mess full of nested callbacks ... oh, irony

# C. Scott Ananian (9 years ago)

I don't see any evidence that you can't handle the various cases with the usual control flow mechanisms. Remember that promises are isomorphic to the future async/await syntax.

# Andrea Giammarchi (9 years ago)

async and await would fail here same way indeed, there should be a mechanism to branch out the binary expected result, IMO.

The other thread and the fact .NET already has something similar should probably ring some bell but it's probably there that we should discuss this.

# C. Scott Ananian (9 years ago)

Again, I don't see how this requires some new sort of control flow.

# Andrea Giammarchi (9 years ago)

try to put a cancel in your same code:

function tryToAscend() {
return Door.close().then(function() { return raiseLift(); },
function(e) { return Door.open().delay(10*1000).then(tryToAscend); });
};

because when the door opens somebody cancel the floor and wants to descend instead.

Then implement the case when actually the call was simply canceled and nobody has to go anywhere ... all while delay keeps saying to each floor that after 10 seconds doors should close anyway.

Now try to pass this glorious flow around per each panel in each floor that should behave accordingly.

Can you do this via Promises? 'cause I'm pretty sure that code won't look any better than an event based one, but I can be wrong.

# Cyril Auburtin (3 years ago)

Follow up on esdiscuss.org/topic/promise-returning-delay-function

I think we really need that function, with AbortController support

We could do things like

const abortFetch = new AbortController();
const abortTimeout = new AbortController();

await Promise.race([
  Promise.delay(5000, {signal: abortTimeout.signal}),
  fetch(url, {signal: abortFetch.signal}).then(r => r.json()).then(...),
])
  .finally(() => {
    abortFetch.abort();
    abortTimeout.abort(); // avoid mem leaks, that would call clearTimeout
under the hood
  })
# James M Snell (3 years ago)

For Node.js we implemented this as an alternative to the current setTimeout:

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

// Or

import { setTimeout } from 'timers/promises'

Then...

await setTimeout(500)

It works very well and is straightforward to implement on the browser-side.