0

I have a function that has to do something async, in a few steps. At every step it can fail. It might fail before step 1, so you might know the result immediately, or after 1.5 sec. When it fails, it has to run a callback. Idem when it succeeds. (I'm using when on purpose, because it's not just if: the timing is important.)

I thought Promises are perfect, because async and they resolve only once, but it still has a problem: when does it fail? I can explictly see when it succeeds (after the last step), but when does it fail? Inside/before any step.

This is what I have now, but that's ridiculous:

function clickSpeed() {
    return new Promise(function(resolve, reject) {
        if ( test('one') ) {
            return setTimeout(function() {
                if ( test('two') ) {
                    return setTimeout(function() {
                        if ( test('three') ) {
                            return setTimeout(function() {
                                console.log('resolving...');
                                resolve();
                            }, 500);
                        }
                        console.log('rejecting...');
                        reject();
                    }, 500);
                }
                console.log('rejecting...');
                reject();
            }, 500);
        }
        console.log('rejecting...');
        reject();
    });
}

(test() randomly passes or fails a step.)

Fiddle here: http://jsfiddle.net/rudiedirkx/zhdrjjx1/

I'm guessing the solution is to chain promises, which resolves or rejects every step..? Maybe. Is that a thing? How would I implement that?

Could this work for an unknown number of steps?

Rudie
  • 52,220
  • 42
  • 131
  • 173
  • 2
    It fails as soon as you let it fail. Not sure I understand what the question is. – Bergi Nov 23 '15 at 19:16
  • What is expected result of `return` at `return setTimeout(function() {` ? – guest271314 Nov 23 '15 at 19:19
  • 1
    @guest271314: I think the `return` is only there to prevent the `console.log('rejecting...'); reject();` from running – Bergi Nov 23 '15 at 19:20
  • @Bergi When do I let it fail? The steps are async, so I don't know if the next step will succeed. Do I have to check every step explicitly, as in above code? – Rudie Nov 23 '15 at 19:20
  • @guest271314 The return is just to break the flow, so it doesn't reject. – Rudie Nov 23 '15 at 19:21
  • I think u want smth like this : `promise.then(function(res, reject) { res('step1') or reject('err')}).then(function(res) { res('step2')}).catch(function(err) { console.log(err) })` – Errorpro Nov 23 '15 at 19:21
  • @Rudie: Yes, you have to explicitly let it fail in every step, because if you don't, it wouldn't fail. Of course, you should be using promises quite differently. – Bergi Nov 23 '15 at 19:23
  • _"but when does it fail? Inside/before any step."_ , _"The return is just to break the flow, so it doesn't reject. "_ `test` does not reject promise ? How would `reject` be called based on return value of `test` , if `setTimeout` "break the flow" ? – guest271314 Nov 23 '15 at 19:26
  • @Bergi Differently how? Of course why? – Rudie Nov 23 '15 at 19:26
  • @guest271314 `test()` only returns a bool. It might not be a function, but a statement, or check another object/property. The rejection happens **after** every IF. – Rudie Nov 23 '15 at 19:27
  • @guest271314 `test()` returns a bool, but the random is only for testing. It takes anything from anywhere else, which can reject the promise, or advance it to the next step. The`test()` check isn't always a function and doesn't know about promises. – Rudie Nov 23 '15 at 19:37

3 Answers3

2

You could rewrite your solution to promises quite literally:

function sleep(ms) {
    return new Promise(function(resolve) {
        setTimeout(resolve, ms);
    });
}

function clickSpeed() {
    if ( test('one') ) {
        return sleep(500).then(function() {
            if ( test('two') ) {
                return sleep(500).then(function() {
                    if ( test('three') ) {
                        return sleep(500).then(function() {
                            console.log('resolving...');
                        });
                    }
                    console.log('rejecting...');
                    return Promise.reject();
                });
            }
            console.log('rejecting...');
            return Promise.reject();
        });
    }
    console.log('rejecting...');
    return Promise.reject();
}

However, that's still quite ugly. I'd rather reverse the if/elses as a first step:

function clickSpeed() {
    if (!test('one')) {
        console.log('rejecting...');
        return Promise.reject();
    }
    return sleep(500).then(function() {
        if (!test('two')) {
            console.log('rejecting...');
            return Promise.reject();
        }
        return sleep(500).then(function() {
            if (!test('three')) {
                console.log('rejecting...');
                return Promise.reject();
            }
            return sleep(500).then(function() {
                console.log('resolving...');
            });
        });
    });
}

But then we also can unnest these callbacks. It's usually not possible when you're doing branching with if, but in this case the only alternative result is a rejection, which is like throwing, and won't execute the chained then callbacks.

function clickSpeed() {
    return Promise.resolve() // only so that the callbacks look alike, and to use throw
    .then(function() {
        if (!test('one')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500);
    }).then(function() {
        if (!test('two')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500)
    }).then(function() {
        if (!test('three')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500);
    }).then(function() {
        console.log('resolving...');
    });
}

Now you could make those tests throw the exception itself so you don't need the if any more, and you could move the console.log('rejecting...'); statement in a .catch().

And while I guess it's only your example where all steps look alike, you could easily create the promise chain dynamically from a list as well:

function clickSpeed() {
    return ['one', 'two', 'three'].reduce(function(p, cur) {
        return p.then(function() {
            if (!test(cur))
                throw new Error('rejecting...');
            else
                return sleep(500);
        });
    }, Promise.resolve());
}
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Can describe effect of `throw` at post ? – guest271314 Nov 23 '15 at 19:39
  • That's what I was playing with, sort of, but couldn't figure out: http://jsfiddle.net/rudiedirkx/zhdrjjx1/1/ This looks more like it! – Rudie Nov 23 '15 at 19:41
  • 1
    @guest271314: `throw;` is no different than `return Promise.reject();` when inside a `then` callback. – Bergi Nov 23 '15 at 19:41
  • @Rudie: Ah, I missed to look at your fiddle. It suffers from the [`Promise` constructor antipattern](http://stackoverflow.com/q/23803743/1048572), and doesn't work because the call to `reject()` doesn't break the chain like an exception. – Bergi Nov 23 '15 at 19:43
  • @Bergi What is returned to `onRejected` from `throw` ? Concerning "Promise constructor antipattern" http://stackoverflow.com/q/23803743/1048572 , is premise that `new Promise(function(resolve, reject){})` should not ever be used ? – guest271314 Nov 23 '15 at 19:43
  • @guest271314: Nothing. That is, `undefined`. Just like the argument to `Promise.reject()`. – Bergi Nov 23 '15 at 19:46
  • @guest271314: Concerning the Promise constructor antipattern, please read the answers to the linked question. They don't say "never". In fact I use it properly in this very answer (`sleep`). – Bergi Nov 23 '15 at 19:48
  • @Bergi Yes and yes, exactly. I knew there was a good Promises way. This way is even more chainable by the function calling `clickSpeed()` which is exactly what I was hoping for is possible. Thanks! – Rudie Nov 23 '15 at 19:50
  • @Bergi Read page , several occasions now. What is difference between `sleep` as at post and `function sleep(ms) { Promise.resolve().then(function() { /*setTimeout here*/ })}` ? In particular @thefourtheyes Answer reference to http://programmers.stackexchange.com/a/279003 would appear to convey not use constructor at all? , based on "performance" ? Having difficulty here attempting to interpret definitely when, when not to . Would ask Question , though would probably be closed as duplicate . Not entirely clear , here , how to evaluate when to use constructor pattern or `Promise.resolve()` ? – guest271314 Nov 23 '15 at 20:02
  • 1
    @guest271314: Use `Promise.resolve` when you can. Sometimes you cannot, you will need the `Promise` constructor. `sleep` is just one example of that, your implementation simply doesn't work. Try it out to see why. No, this has nothing to do with performance (which would be library-specific, the programmers question you linked seems to discuss Bluebird). – Bergi Nov 23 '15 at 20:05
1

First, let's rearrange the opening statement very subtly to reflect a more typical and more appropriate use of promises.

Instead of :

a function that has to do something async, in a few steps

let's say :

a function that has to do something in a few async steps

Thus, we might first choose to write a function that executes a promise chain and returns its resultant promise :

function doAnAsyncSequence() {
    return Promise.resolve()
    .then(function() {
        doSomethingAsync('one');
    })
    .then(function() {
        doSomethingAsync('two');
    })
    .then(function() {
        doSomethingAsync('three');
    });
}

And, for the sake of demonstration, we can write doSomethingAsync() such that it returns a promise that has a 50:50 chance of being resolved:rejected (which is more useful here than a delay) :

function doSomethingAsync(x) {
    return new Promise(function(resolve, reject) {
        if(Math.random() > 0.5 ) {
            resolve(x);
        } else {
            reject(x); // importantly, this statement reports the input argument `x` as the reason for failure, which can be read and acted on where doSomethingAsync() is called.
        }
    });
}

Then, the central part of the question :

when does it fail?

might be rephrased :

when did it fail?

which is a more realistic question because we would typically be calling asynchronous process(es) over which we have little influence (they might run on some server somewhere else in the world), and which we hope will succeed but may randomly fail. If so, our code (and/or our end-users) would like to know which one failed, and why.

In the case of doAnAsyncSequence(), we can do that as follows :

doAnAsyncSequence().then(function(result) {
    console.log(result); // if this line executes, it will always log "three", the result of the *last* step in the async sequence.
}, function(reason) {
    console.log('error: ' + reason);
});

Although there is no console.log() statement in either doAnAsyncSequence() or doSomethingAsync() :

  • on success, we can observe the overall result (which will always be "three" in this example).
  • on error, we know exactly which of the async steps caused the process to fail ("one", "two" or "three").

Try it out


So that's the theory.

To answer the specific question (as I understand it) ...

for doSomethingAsync(), write :

function test_(value, delay) {
    return new Promise(function(resolve, reject) {
        //call your own test() function immediately
        var result = test(value);
        // resolve/reject the returned promise after a delay
        setTimeout(function() {
            result ? resolve() : reject(value);
        }, delay);
    });
}

for doAnAsyncSequence(), write :

function clickSpeed() {
    var delayAfterTest = 500;
    return Promise.resolve()
    .then(function() {
        test_('one', delayAfterTest);
    })
    .then(function() {
        test_('two', delayAfterTest);
    })
    .then(function() {
        test_('three', delayAfterTest);
    });
}

And call as follows :

clickSpeed().then(function() {
    console.log('all tests passed');
}, function(reason) {
    console.log('test sequence failed at: ' + reason);
});
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • Very readable, but I do need the timeout, because other things do things async and I have to wait ~ 500ms. – Rudie Nov 26 '15 at 08:42
  • As I say, my `doSomethingAsync()` is for demonstration. In your real-world code, you may well choose to write something completely different. – Roamer-1888 Nov 26 '15 at 09:56
1

Since your timing is monotone, and your "work" is predefined, I'd refactor the code to utilize setInterval() with a context to keep track of required "work" or "step". In fact, you don't even have to use a Promise when doing so, although you still can, and if you want to further chain handlers it's a good idea:

function clickSpeed(resolve, reject) {
  var interval = setInterval(function(work) {
    try {
      var current = work.shift();
      if(!test(current)) { // Do current step's "work"
        clearInterval(interval); // reject on failure and clear interval
        console.log('rejecting...', current);
        reject();
      }
      else if(!work.length) { // If this was the last step
        clearInterval(interval); // resolve (success!) and clear interval
        console.log('resolving...');
        resolve();
      }
    }
    catch(ex) { // reject on exceptions as well
      reject(ex);
      clearInterval(interval);
    }
  }, 500, ['one', 'two', 'three']); // "work" array
}

The function can either be called "directly" with resolve/reject handlers, or used as the argument of the Promise constructor.

See full example in modified JSFiddle.


To address Bergi's too much boilerplate comment, code can be written more concisely, without logging:

function clickSpeed(resolve, reject) {
    function done(success, val) {
        clearInterval(interval);
        success ? resolve(val) : reject(val);
    }

    var interval = setInterval(function(work) {
        try {
            if(test(work.shift()) || done(false)) {
                work.length || done(true);
            }
        }
        catch(ex) { // reject on exceptions as well
            done(false, ex);
        }
    }, 500, ['one', 'two', 'three']); // "work" array
}
Amit
  • 45,440
  • 9
  • 78
  • 110
  • Hey, that's pretty smart! I like it. – Rudie Nov 26 '15 at 08:39
  • I'd recommend to avoid `setInterval` when using promises, it just doesn't fit in. And you loose throw safety, and have to go back to messing with callbacks. Also, much too much boilerplate… The monotony can easily be utilised in promise code just as easily (see the update to my answer). – Bergi Nov 26 '15 at 10:47
  • @Bergi - The solution doesn't really use promises, but "enables" wrapping with a Promise. Exception handling is baked inside, and callback mechanism is by design (as OP described). Boilerplate is minimal (as shown in the updated version) and is comparable to your solution - which I think is also a good one. – Amit Nov 26 '15 at 12:13
  • Oops, I must have overread that `try catch` statement, I'll take that back. Still I guess the necessity for `try` is exactly what makes your solution a bit longer than mine :-) – Bergi Nov 26 '15 at 12:30