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:
async function executeInSeries(tasks): Theasynckeyword declares this function as asynchronous. This means it will always return a Promise, and we can use theawaitkeyword inside it. The function takes an arraytasksas input, where each element is expected to be a function that returns a Promise.for (const task of tasks) { ... }: We loop through eachtaskin the providedtasksarray.const result = await task();: This is where the magic ofawaithappens. Theawaitkeyword pauses the execution of theexecuteInSeriesfunction until the Promise returned bytask()either resolves (completes successfully) or rejects (encounters an error). This guarantees that each task finishes before the next one starts.try...catch: This block is crucial for handling potential errors. If a Promise returned by a task rejects, thecatchblock 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 newPromise. - Inside the Promise’s executor,
setTimeoutis 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:
- The
forloop begins withtask1.await task1()is called, and theexecuteInSeriesfunction pauses for 100ms. - After 100ms,
task1‘s Promise resolves, and'Task 1 done'is added to theresultsarray. - The loop moves to
task2.await task2()is called, and the function pauses for another 50ms. - After 50ms,
task2‘s Promise resolves, and'Task 2 done'is added to theresultsarray. - The loop finishes, and the
executeInSeriesPromise 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.



