≡ Menu

Understanding Async/Await and Sequential Task Execution in JavaScript

In the world of JavaScript, especially when dealing with operations that might take some time (like fetching data from an API, reading files, or setting timers), the concept of asynchronous programming is paramount.

It allows our applications to remain responsive even when performing long-running tasks.

Two powerful keywords that have revolutionized asynchronous JavaScript are async and await.

Today, let’s dive into a practical example of how async/await can be used to execute a series of asynchronous tasks sequentially, ensuring that each task completes before the next one begins.

We’ll dissect a concise piece of code that elegantly achieves this.

The executeInSeries Function

 

Imagine you have a list of operations that need to be performed in a specific order, and some of these operations are asynchronous (meaning they might take some time to finish). The executeInSeries function provides a neat solution:

/**
 * Executes a series of asynchronous tasks one after another.
 * @param {Array<Function>} tasks - An array of functions, each returning a Promise.
 * @returns {Promise<Array>} A promise that resolves with an array of results from each task.
 */
async function executeInSeries(tasks) {
  const results = [];
  for (const task of tasks) {
    try {
      const result = await task();
      results.push(result);
    } catch (error) {
      console.error('Task failed:', error);
      // Pushing the error object allows the series to continue.
      // You could also re-throw the error to stop the execution.
      results.push(error); 
    }
  }
  return results;
}

Let’s break down what’s happening here:

  1. async function executeInSeries(tasks): The async keyword declares this function as asynchronous. This means it will always return a Promise, and we can use the await keyword inside it. The function takes an array tasks as input, where each element is expected to be a function that returns a Promise.
  2. for (const task of tasks) { ... }: We loop through each task in the provided tasks array.
  3. const result = await task();: This is where the magic of await happens. The await keyword pauses the execution of the executeInSeries function until the Promise returned by task() either resolves (completes successfully) or rejects (encounters an error). This guarantees that each task finishes before the next one starts.
  4. try...catch: This block is crucial for handling potential errors. If a Promise returned by a task rejects, the catch block will gracefully handle the error, preventing the entire program from crashing.

Seeing It in Action: A Simple Example

Let’s create a couple of asynchronous “tasks” using setTimeout to simulate delays. We’ll use a Promise to represent an operation that will complete in the future.

// Task 1: A function that returns a Promise resolving after 100ms
const task1 = () => new Promise(resolve => setTimeout(() => resolve('Task 1 done'), 100));

// Task 2: A function that returns a Promise resolving after 50ms
const task2 = () => new Promise(resolve => setTimeout(() => resolve('Task 2 done'), 50));

// Execute the tasks in series
executeInSeries([task1, task2])
  .then(results => console.log('Series results:', results));

Understanding the task Function

The task1 function is a great example of how to wrap a delayed operation in a Promise:

const task1 = () => new Promise(resolve => setTimeout(() => resolve('Task 1 done'), 100));

This code snippet does the following:

  • It defines an arrow function task1.
  • When task1() is called, it immediately returns a new Promise.
  • Inside the Promise’s executor, setTimeout is called. It schedules a callback function to run after 100 milliseconds.
  • After the 100ms delay, the callback executes and calls resolve('Task 1 done'), which fulfills the Promise.

The Execution Flow

When we run executeInSeries([task1, task2]), the program follows a clear, sequential path:

  1. The for loop begins with task1. await task1() is called, and the executeInSeries function pauses for 100ms.
  2. After 100ms, task1‘s Promise resolves, and 'Task 1 done' is added to the results array.
  3. The loop moves to task2. await task2() is called, and the function pauses for another 50ms.
  4. After 50ms, task2‘s Promise resolves, and 'Task 2 done' is added to the results array.
  5. The loop finishes, and the executeInSeries Promise resolves with the final array of results: ['Task 1 done', 'Task 2 done'].

This ensures that even though task2 has a shorter delay, it will always be executed after task1, producing a predictable and ordered output.

Why is This Useful?

Executing asynchronous tasks in series is crucial in scenarios where the outcome of one task depends on the completion of a previous one. For example:

  • Database Operations: Writing data to one table and then using the ID from that new record to write data to another table.
  • API Chains: Fetching a user ID from one API endpoint and then using that ID to retrieve the user’s profile from a second endpoint.
  • File Processing: Reading a file, processing its contents, and then writing the modified data to a new file.

The async/await syntax makes this kind of sequential asynchronous logic much cleaner and easier to read compared to traditional Promise chaining with .then().

By combining these keywords with a simple loop, we can build robust and predictable asynchronous workflows.

{ 0 comments… add one }

Leave a Comment