0

Sometimes I want to defer the execution of the promise constructor and still be able to define promise chains. I found and slightly adapted the following approach, but since I have little experience with promises I'd like to know

  • whether there is a more succinct way to do this?
  • what I am losing with this approach?

class Deferred {
  constructor() {
    this.promise = new Promise((r, e) => {
      // arrows use this of their surrounding scope
      this.resolve = r;
      this.reject = e;
    });

    this.then = this.promise.then.bind(this.promise);
    this.catch = this.promise.catch.bind(this.promise);
    this.finally = this.promise.finally.bind(this.promise);
  }
}

const sqrAsync = n => {
  const d = new Deferred;
  
  d.runPromise = () => typeof n === "number"
    ? setTimeout(d.resolve, 0, n * n)
    : setTimeout(d.reject, 0, new Error("number expected"));
   
  return d;
};

const deferred = sqrAsync(5),
  deferred2 = sqrAsync("foo");

deferred
  .then(console.log)
  .catch(console.error)
  .finally(() => console.log("clean up 1st"));

deferred2
  .then(console.log)
  .catch(console.error)
  .finally(() => console.log("clean up 2nd"));

deferred.runPromise();
deferred2.runPromise();

I know that I can achieve this and other desirable properties using userland's Task/Furture implementations and I usually do that. However, sometimes I need the ES6 Promise interop.

To anticipate the why: I want to separate "effectful" computations from the rest of my program.

  • 1
    I think there are only rare usecases for this, usually one can put the async code into the promise directly. But if you really need to reject / resolve outside of it this seems the way to go. – Jonas Wilms Mar 13 '18 at 18:16
  • Oh and you dont need the setTimeout, `then`s will only be called one tick after `resolve` was – Jonas Wilms Mar 13 '18 at 18:17
  • I'm testing out your code and `this.promise.finally.bind(this.finally);` should have been `this.promise.finally.bind(this.promise);` – T Tse Mar 13 '18 at 18:27
  • A prior implementation of a Deferred object here: https://stackoverflow.com/questions/37651780/why-does-the-promise-constructor-need-an-executor/37673534#37673534 though usually a Deferred can be completely avoided with appropriate coding. – jfriend00 Mar 13 '18 at 19:20

3 Answers3

1

Honestly, don't do that. Deferreds are deprecated for good reason.

function sqrAsync(n) {
  return new Promise((resolve, reject) => {
    if (typeof n === "number")
      setTimeout(resolve, 0, n * n);
    else
      setTimeout(reject, 0, new Error("number expected"));
  });
}

var start1, start2;

const p1 = new Promise(resolve => { start1 = resolve; }).then(() => sqrAsync(5));
const p2 = new Promise(resolve => { start2 = resolve; }).then(() => sqrAsync("foo"));

p1.then(console.log, console.error).finally(() => console.log("clean up 1st"));
p1.then(console.log, console.error).finally(() => console.log("clean up 2nd"));

start1();
start2();

You should never need to defer the execution of the promise constructor. If you need to wait for something before starting your action that returns a promise - make a promise for that something.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Why are you calling `resolve()` or `reject()` on a `setTimeout()` with a `0` delay? Why is that needed? – jfriend00 Mar 13 '18 at 19:22
  • @jfriend00 I don't know, it's just what the OP had. Probably to have some actual asynchrony in the toy example. Of course `function sqr(n) { if (typeof n == "number") return n*n; else throw new Error("…"); }` would work as well here. – Bergi Mar 13 '18 at 19:24
  • But, `.then()` already gives you asynchrony (didn't know that was a word, but it fits here). – jfriend00 Mar 13 '18 at 19:25
  • @jfriend00 Oh, right, the `0` delay is suspicious, I thought it was just some arbitrary delay. – Bergi Mar 13 '18 at 19:26
  • Sure, this is an alternative, but assignments to vars in the outer scope... For what I know with `Deferred` I only lose promise rejection caused by errors in the sync. applicator. Besides, I want to use `Deferred` solely encapsulated in functions - I can't see anything harmful in that. Anyway, I must admit that the way I want to use the `Promise` type is inconsistent with what it implies: To proxy the result of an async computation. But then again it has a distinct name (`Deferred`), so the modified behavior is somehow explicit. –  Mar 13 '18 at 20:18
  • @ftor Well if you want to call `resolve` from outside then you need to assign it to something accessible from there. If you insist, you can use a `class Deferred { constructor() { this.promise = new Promise(r => { this.resolve = r; }); }}` as a helper, but usually you'd tie the resolve function directly to the functionality where it will be used in. You still haven't stated your actual use case - what are you waiting for to trigger the promise? – Bergi Mar 13 '18 at 22:56
  • The rough idea is to clearly separate the sync form the async portion of my program, that is I only call `runPromise` at the end of the respective scope. Everything above is purely synchronous and sequentially evaluated. This way I can actually cancel promises, that is not just the execution of its `then` handlers but the async operation itself. Usually I use my own `Cont` based `Task` implementation, but as already mentioned I sometimes need Promise interop, including `async`/`await`. –  Mar 14 '18 at 10:31
  • @ftor I'm not sure I understand, isn't the asynchronous `then` chain part of that "synchronous portion" above the `runPromise` call? Maybe it would help if you could post your actual code somewhere (a gist?). Also what do you mean by "cancel promise", isn't that - when doing it synchronously - just what an `if` block around `Promise.resolve()` could do as well? – Bergi Mar 14 '18 at 15:42
  • Hm, considering your confusion I am certainly trying something stupid. All I want to do is to define promise chains without actually triggering the corresponding async operations immediately. Technically, `deferred` may already be an async computation, because it yields a pending promise. But this promise remains pending until I run it. It neither mutates (pending -> settled) nor does it change the evaluation order. So it is basically synchronous and pure. –  Mar 14 '18 at 17:32
  • And yes, conditionally creating a promise is almost equivalent to conditionally running my deferred version, except that with the latter I can decide whether to run the promise or not at the last possible moment, regardless of where it is lexically defined. –  Mar 14 '18 at 17:33
  • @ftor Do you ever need the promise to stay pending (and still use it)? If not, then you probably are trying something stupid. I think you should just put the promise chain in a function, possibly compose some functions with more or less complicated logic, and then in the end you can still decide whether or not to call the function that then returns the promise. – Bergi Mar 14 '18 at 17:40
1

re: a more succinct way to do this

Your sqrAsync function can be rewritten like so:

function sqrAsync(n) {
  if (typeof n === 'number')
    return Promise.resolved(n * n);
  return Promise.reject(new Error('number expected'));
}

Or, using async:

async function sqrAsync(n) {
  if (typeof n === 'number')
    return n * n;
  throw new Error('number expected');
}

Your Deferred is indeed more flexible, but I agree with Jonas in that the use case is rare.

re: what I am losing

One thing that you lose is that by doing this you assumed the Promise prototype to always have then, catch, finally and nothing else. You should consider extending Promise if you really want to do this. e.g. in node.js, finally is not supported (supported since v8.1.4+ behind a cli flag) and your code would throw a "Cannot read property 'bind' of undefined" error.

As @Bergi has pointed out in comments, not binding any methods would work around the problem for us assuming the prototype of Promise. This might make the class harder to use. An alternative might be looping through ownProperties of Promise to find out the list of methods. I don't want to be the one to maintain this code though.

T Tse
  • 786
  • 7
  • 19
  • Instead of binding all those methods, one could simply start the chain with `deferred.promise.then(…)` – Bergi Mar 13 '18 at 19:25
  • @Bergi That's jQuery specific and I am more familiar with node. If you (OP) are specifically coding for browser environment and is using jQuery, then sure, go ahead. – T Tse Mar 13 '18 at 19:44
  • No, it's not jQuery-specific at all, it's the code from the OP's `Deferred` class which has a `.promise` property – Bergi Mar 13 '18 at 22:57
  • @Bergi Ah, I see what you mean now. However, this answer doesn't even have OP's `Deferred` class. Not binding the methods make the class harder to use. I'll update the answer – T Tse Mar 13 '18 at 23:18
0

From the MDN documentation:

The executor function is executed immediately by the Promise implementation, passing resolve and reject functions (the executor is called before the Promise constructor even returns the created object).

A Promise is designed to abstract the behavior of asynchronous execution, and possible failure. What you are looking for is delayed execution, or possibly lazy execution. I don't think a Promise will provide you an easy way to achieve that.

Robert Stiffler
  • 675
  • 3
  • 10