1
\$\begingroup\$

I implemented a function for practice that takes a collection of asynchronous functions and a callback. It executes the functions in sequence then calls the callback with the results (coming from the callbacks of the async functions). It partially mimics the behavior of series function in the async library

The function

const series = (tasks, cb) => {
    let results = [];
    const iterator = index => {
        if (index === tasks.length) {
            return;
        }
        const task = tasks[index];
        task(val => {
            results.push(val);
            iterator(index + 1);
            if (index === tasks.length - 1) {
                cb(results);
            }
        });
    }
    iterator(0);
}

Usage

const tasks = [
    cb => {
        setTimeout(() => {
            cb('done 1');
        }, 1000);
    },
    cb => {
        setTimeout(() => {
            cb('done 2');
        }, 1000);
    },
    cb => {
        setTimeout(() => {
            cb('done 3');
        }, 1000);
    }
];

series(tasks, results => {
    console.log(results);
});

// outputs ['done 1', 'done 2', 'done 3'] after 3 seconds

What do you think about my approach? How would you implement it? Using plain JavaScript.

\$\endgroup\$
2
\$\begingroup\$

Quick review.

It is not bullet proof. So much so that it is a bug dressed in javascript.

You must protect against improper use, inadvertent error, or failure to complete.

The next bit of code shows a variety of ways it will fail.

series([cb => {
    if (Math.random() < 0.01) { throw "Spanner" }
    const handle = setTimeout(() => {
        cb('done');
        console.log("Not really done");
        cb('Done again');
    }, 1000);
    // Indeterminate completing clause
    setTimeout(() => clearTimeout(handle), Math.random() * 2000);
}, "Iron filings"], undefined); // undefined for a free throw if it got to the end

Why not use promises and async functions???

The following is immune to any of the above spannering.

async function series(tasks, tasksComplete) {
    if (Array.isArray(tasks)) {
        const timeoutPeriod = 5000; // 5 seconds and your out. Task can still complete
        const results = [];
        while (tasks.length) {
            const task = tasks.shift();
            if (typeof task === "function") {
                await new Promise(done => {
                        setTimeout(() => done("Too slow!") , timeoutPeriod);
                        task(done);
                    })
                    .then(result => results.push(result))
                    .catch(error => results.push(error))  // assuming good to continue
            }
        }
        if (typeof taskComplete === "function") { tasksComplete(results) }
    }
}

Update

In lieu of the above answer and comment you made the following might suit your needs a little better.

Rather than pass a callback to the series function, that function returns a promise which will resolve when all tasked have completed successfully, if there is a problem along the way the function will terminate returning the error.

Also the results stack is passed to each task. Rather than the last results the entire results stack is passed. It is up to the task which results it needs to use.

Also added are some options. Stop on error if true will stop on the first error, and timeout is greater then 0 will have a timeout error on tasks.

// Returns promise to do tasks in series
// stopOnError is false then continues tasks if there is an error placing
//             the error on the results stack
// timeout if > 0 will throw error if task takes too long.
// Promise resolves with the results stack for both completed and error
function series(tasks, stopOnError = true, timeout = 0) {
    const results = [];
    async function doEachTask(completed, failed) {
        while (tasks.length) {
            const task = tasks.shift();
            if (typeof task === "function") {
                await new Promise((done,error) => {
                        if (timeout) { 
                            setTimeout(()=> error(new Error("Too slow!")) , timeout);
                        }
                        task(done, results);
                    })
                    .then(result => results.push(result))
                    .catch(error => {
                        results.push(error);
                        if (stopOnError) { failed(results) }
                    });

            }
        }
        completed(results);
    }
    return new Promise(doEachTask);
}

Used as follows

series([
    (complete, data) => {
         setTimeout(()=> {
             complete("Task1 complete");
         },1000)
    },
    (complete, data) => {
         console.log("Task 2 has data : "+data[0]);
         setTimeout(()=> {
             complete("Task2 complete");
         },1000)
    }])
    .then(results => console.log(results))
    .catch(results => console.log("Error and available results : " + results));
\$\endgroup\$
  • \$\begingroup\$ That looks good! I didn't do any error handling, I believe it's doable. The only catch is, what if I want to pass the result of every function to the next one? It's straightforward in my approach. What do you think? \$\endgroup\$ – Mark Adel Mar 22 '18 at 0:07
  • 1
    \$\begingroup\$ @MarkAdel Previous task result can be passed to the next task by replacing the line task(done) with if (results.length > 0) { task(done, results[results.length -1]) } else { task(done) } . Please note I answered because I found it strange you did not use promises and async functions. The example given made a lot of assumptions regarding timeout and errors and may not be best suited to your particular needs. You may wish to terminate by exiting series on error and or timeout. You can use a semaphore, set in catch and or timeout to exit after the await line. \$\endgroup\$ – Blindman67 Mar 22 '18 at 0:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.