Implementing N Async Tasks in Series in JavaScript



Implementing a function to execute N asynchronous tasks in series means ensuring that each task runs only after the previous one has completed. This is particularly useful when tasks have dependencies on the results of prior tasks or when tasks need to run in a specific order.


What is Series Execution?

Series execution involves running tasks one after the other, ensuring that each task starts only after the previous task has finished. This approach is essential when tasks are interdependent or need to execute sequentially.

Real Interview Insights

Interviewers might ask you to:

  • Implement a function to execute an array of asynchronous tasks in series.
  • Ensure proper handling of asynchronous operations using Promises or async/await.
  • Manage error handling appropriately within the series execution.

Implementing runTasksInSeries Function

Here’s an implementation of a function to run N async tasks in series:

function runTasksInSeries(tasks) {
  return tasks.reduce((promiseChain, currentTask) => {
    return promiseChain.then(chainResults =>
      currentTask().then(currentResult => [...chainResults, currentResult])
    );
  }, Promise.resolve([]));
}
Explanation:
  • Reduce Method: Use Array.prototype.reduce to create a promise chain.
  • Initial Value: Start with a resolved promise (Promise.resolve([])).
  • Promise Chain: For each task, wait for the previous task to complete before running the next one.
  • Result Accumulation: Accumulate results in an array.

Practical Examples

Consider an example with async tasks:

const tasks = [
  () => new Promise(resolve => setTimeout(() => resolve('Task 1'), 1000)),
  () => new Promise(resolve => setTimeout(() => resolve('Task 2'), 500)),
  () => new Promise(resolve => setTimeout(() => resolve('Task 3'), 300))
];
 
runTasksInSeries(tasks).then(results => {
  console.log(results); // Output: ['Task 1', 'Task 2', 'Task 3']
});

Handling Edge Cases

  1. Empty Task Array: Handle cases where the array of tasks is empty.
  2. Error Handling: Ensure that errors in any task are properly caught and handled.

Enhanced Implementation with Error Handling

function runTasksInSeries(tasks) {
  return tasks.reduce((promiseChain, currentTask) => {
    return promiseChain.then(chainResults =>
      currentTask()
        .then(currentResult => [...chainResults, currentResult])
        .catch(error => {
          console.error(`Error in task: ${error.message}`);
          return [...chainResults, null]; // Or handle the error as needed
        })
    );
  }, Promise.resolve([]));
}
 
// Example usage with error handling
const tasks = [
  () => new Promise(resolve => setTimeout(() => resolve('Task 1'), 1000)),
  () => new Promise((resolve, reject) => setTimeout(() => reject(new Error('Task 2 failed')), 500)),
  () => new Promise(resolve => setTimeout(() => resolve('Task 3'), 300))
];
 
runTasksInSeries(tasks).then(results => {
  console.log(results); // Output: ['Task 1', null, 'Task 3']
});

Use Cases for Series Execution

  1. Dependent Tasks: When each task relies on the output of the previous one.
  2. Ordered Execution: Ensuring tasks run in a specific order.
  3. Rate Limiting: Controlling the rate of task execution to avoid overwhelming resources.