Dynamically Chaining Async Tasks Using Array Reducers

Last Updated On 15 Jan 2023 by

In a perfect Async JS world one would chain async task in the following way:

async function chainTasks () {
  await doSomething();
  await doSomethingElse();
  await doSomethingOtherThing();
  await weAreDone(); // 🤗🤗🤗
}

But sometimes you needs to work on a legacy stack, or maybe you’re working in Cypress and can’t use async functions. A common alternative approach would be to use Promises to chain tasks as such:

function chainTasks () {
  return new Promise(weAreDone => {
    doSomething()
      .then(doSomethingElse)
      .then(doSomethingOtherThing)
      .then(weAreDone); // 🤗🤗🤗
  });
}

Now sometimes things can get a bit more demanding. Let’s imagine a case where tasks can not be predetermined in advance and need to be concatenated together at run time, instead of when writing the code as shown earlier.

Let’s picture an hypothetical scenario where you are part of a large company that manages several apps and websites. We’re in charge of building a testing platform that would accomodate the needs of all the teams that work on these apps and websites.

For example when testing app A, we need to make sure service A is restarted as well as making sure that test data is properly populated on the database prior to the test run.

When testing website B however, we don’t rely on any service but we need to send a report to the A team when the test is complete so instructions would be very different than for app A.

The platform could maintain a profile for each app and handle each test request according to each profile. But this means shifting ownership of the test pipeline from the app/website owners to the platform owners. Instead we would prefer a more flexible system where the developers can request a test run by submitting their own set of instructions/tasks in a array of commands and parameter.

Following this example, app A developers would submit the following request to the testing platform:

[
  { command: 'restart-service', params: [ 'service-a' ]},
  { command: 'run-test-suites', params: [ 'visual-tests', 'ui-integration' ]},
]

While Website B developers would want to submit a different set of commands:

[
  { command: 'run-test-suites', params: [ 'performance', 'accessibility' ]},
  { command: 'submit-report', params: ['a-team'] },
]

At the core these messages are being received and handled by our processing platform that reads the set of instructions and runs each commands one after the other.

Now we have a real life use case example, let’s take a look at the implementation. First of all, for the sake of simplicity we are not going to implement all the commands available in our testing platform. Instead we will use a dummy function that simulates an async task of any duration we like by wrapping a timer inside a promise:

const availableTasks = {
  // Simulate tasks of different durations
  // usage: simulateTask('task-name', 500);
  simulateTask (id, duration) { 
    return new Promise(done => {
        setTimeout(() => {
            console.log(`${id} is done!`);
            done();
        }, duration)
    });
  },
  // ... add more tasks here
}

In our very simplified implementation, a set of instructions would look like the code below, where each task is added with their respective parameters:

const instructionsSet = [
  { command: 'simulateTask', params: [ 'task-1', 1000 ] },
  { command: 'simulateTask', params: [ 'task-2', 1500 ] },
  { command: 'simulateTask', params: [ 'task-3', 2000 ] },
  { command: 'simulateTask', params: [ 'task-4', 700  ] },
  { command: 'simulateTask', params: [ 'task-5', 500  ] },
]

Now this is where things get interesting, or dynamic I should say. Array.reduce can be used to reduce the dataset of instructions to a chain of promisified tasks.

In the code below, we loop over the set of instructions, promisifying each task and generating a promise chain that can then be chained in our application, all dynamically.

instructionsSet.reduce((chain, {command, params}, index) => {
    console.log(`chaining #${index+1} with params: ${params}`);
    return chain.then(() => availableTasks[command](...params));
}, Promise.resolve()).then(() => console.log('All done! 🤗🤗🤗'));

Similar to Promise.all it takes an array of promises, but instead of running them all at the same time, each task waits for the one ahead to complete before it runs.

Setup vue-cli

Below is the final code, try running it in your JS console and modify the order and durations of tasks. Maybe try adding different tasks. Hope this helps!

const availableTasks = {
  simulateTask (id, duration) { 
    return new Promise(done => {
        setTimeout(() => {
            console.log(`${id} is done!`);
            done();
        }, duration)
    });
  }, // ... add more tasks here
}

const instructionsSet = [
  { command: 'simulateTask', params: [ 'task-1', 1000 ] },
  { command: 'simulateTask', params: [ 'task-2', 1500 ] },
  { command: 'simulateTask', params: [ 'task-3', 2000 ] },
  { command: 'simulateTask', params: [ 'task-4', 700  ] },
  { command: 'simulateTask', params: [ 'task-5', 500  ] },
]

instructionsSet.reduce((chain, {command, params}, index) => {
    console.log(`chaining #${index+1} with params: ${params}`);
    return chain.then(() => availableTasks[command](...params));
}, Promise.resolve()).then(() => console.log('All done! 🤗🤗🤗'));

About The Author

Headshot of Michael Iriarte aka Mika

Hi, I'm Michael aka Mika. I'm a software engineer with years of experience in frontend development. Thank you for visiting tips4devs.com I hope you learned something fun today! You can follow me on Twitter, see some of my work on GitHub, or read more about me on my website.