Search code examples
javascriptjquerypromisedeferredsequential

How can I run multiple functions sequentially, and stop all when any of them fail?


I have multiple Javascript functions, each does some DOM manipulation and then runs an ajax request. I want to be able to run the first function, which manipulates the DOM and then fires its ajax request, then, when the ajax request finishes, I want to run the second function if the ajax request returned true, or stop execution of the rest of the functions and do some other DOM manipulation (like showing an error message).

I want these functions to run sequentially, one after the other, and only keep going if none of them returns false from their ajax request. If none of them returns false, then all of them should run and eventually I would do some manipulation I have inside my "always" callback.

How can I accomplish this?

My first thought was to use promises, to keep the code cleaner, but after several hours of reading I just can't get how to make this work.

Below is my current code, and this is what I get in my console when it executes:

inside test1
inside test2
inside test3
fail
[arguments variable from fail method]
always
[arguments variable from always method]

This is what I want to get in my console (note the missing "inside test3" string):

inside test1
inside test2
fail
[arguments variable from fail method]
always
[arguments variable from always method]

Here's the code:

(function($) {
    var tasks = {
        init: function() {
            $.when(
                this.test1(),
                this.test2(),
                this.test3()
            ).done(function() {
                console.log('done');
                console.log(arguments);
            })
            .fail(function() {
                console.log('fail');
                console.log(arguments);
            })
            .always(function() {
                console.log('always');
                console.log(arguments);
            });
        },

        test1: function() {
            console.log('inside test1');
            return $.getJSON('https://baconipsum.com/api/?type=meat-and-filler');
        },

        test2: function() {
            console.log('inside test2');
            // note the misspelled "typ" arg to make it fail and stop execution of test3()
            return $.getJSON('https://baconipsum.com/api/?typ=meat-and-filler');
        },

        test3: function() {
            console.log('inside test3');
            return $.getJSON('https://baconipsum.com/api/?type=meat-and-filler');
        }
    };

    tasks.init();
})(jQuery)

Any ideas?


Solution

  • Using promises does make this code far cleaner

    Note: Promise/A+ promise .then and .catch callbacks only ever take one argument, so no need for looking at arguments, just use a single argument

    This code (ES2015+) shows how easy

    let p = Promise.resolve();
    Promise.all([this.test1, this.test2, this.test3].map(fn => p = p.then(fn()))).then(allResults =>
    

    alternatively, using array.reduce

    [this.fn1, this.fn2, this.fn3].reduce((promise, fn) => promise.then(results => fn().then(result => results.concat(result))), Promise.resolve([])).then(allResults =>
    

    In both cases, allResults is an array of (resolved) results from the testN functions

    It creates a (resolved) promise to "start" the chain of promises

    the Array#map chains each function (this.test1 etc) to be executed in .then of the previous result. The results of each this.testn promise are returned in a new array, which is the argument for Promise.all

    if any of testN fail, the next wont be executed

    var tasks = {
        init () {
            let p = Promise.resolve();
            Promise.all([this.test1, this.test2, this.test3].map(fn => p = p.then(fn())))
            .then(results => {
                console.log('done');
                console.log(results);
                return results; // pass results to next .then
            }).catch(reason => {
                console.log('fail');
                console.log(reason);
                return reason; // because I return rather than throw (or return a Promise.reject), 
                //the next .then can will get `reason` in it's argument
            }).then(result => {
                console.log('always');
                console.log(result);
            });
        },
        test1() {
            return $.getJSON('https://baconipsum.com/api/?type=meat-and-filler');
        },
        test2() {
            return $.getJSON('https://baconipsum.com/api/?typ=meat-and-filler');
        },
        test3() {
            return $.getJSON('https://baconipsum.com/api/?type=meat-and-filler').then(result => {
                if(someCondition) {
                    throw new Error("sum ting wong");
                    // or
                    return Promise.reject(new Error("sum ting wong"));
                }
                return result;
            });
        }
    };
    tasks.init();
    

    for the code in the question, you could simplify it further

    var tasks = {
        init () {
            let p = Promise.resolve();
            const urls = [
                'https://baconipsum.com/api/?type=meat-and-filler',
                'https://baconipsum.com/api/?typ=meat-and-filler',
                'https://baconipsum.com/api/?type=meat-and-filler'
            ];
            Promise.all(urls.map(url => p = p.then(() => $.getJSON(url))))
            .then(results => {
                console.log('done');
                console.log(results);
                return results; // pass results to next .then
            }).catch(reason => {
                console.log('fail');
                console.log(reason);
                return reason; // because I return rather than throw (or return a Promise.reject), 
                //the next .then can will get `reason` in it's argument
            }).then(result => {
                console.log('always');
                console.log(result);
            });
        }
    };
    tasks.init();