Asynchronous programming (callbacks, async-await, promises) in JS

Gurinderpal Singh Narang
6 min readJan 4, 2024

Asynchronous programming is a programming paradigm that allows for the execution of multiple tasks or processes at the same time without waiting for each other to complete. In traditional synchronous programming, tasks are executed sequentially, one after the other, and each task must be completed before the next one starts. In contrast, asynchronous programming enables tasks to be initiated and executed independently of the main program flow.

The key idea behind asynchronous programming is to handle tasks that may take some time to complete (such as I/O operations, network requests, or file operations) without blocking the execution of the entire program. Instead of waiting for a task to finish before moving on to the next one, asynchronous programming allows the program to continue its execution and handle the completion of tasks when they are done.

JavaScript, especially in the context of web development, is a language where asynchronous programming is commonly used. This is because many operations, like fetching data from a server or handling user input, are asynchronous. Asynchronous programming in JavaScript is often achieved through the use of callbacks, promises, and the async/await syntax.

Here are some key concepts related to asynchronous programming in JavaScript:

  1. Callbacks: Functions that are passed as arguments to other functions and are executed after the completion of a particular task.
  2. Promises: Objects representing the eventual completion or failure of an asynchronous operation. They provide a more structured way to handle asynchronous code compared to callbacks.
  3. Async/Await: A more recent syntax introduced in ECMAScript 2017 (ES8) that simplifies asynchronous code by allowing you to write it in a more synchronous-looking style. The async keyword is used to define a function that returns a promise and the await keyword is used to pause the execution of the function until the promise is resolved.

First let’s understand callback, promises, and async/await with a simple code:

let students = [{
name: "Harry potter",
house: "Gryffindor"
}, {
name: "Draco Malfoy",
house: "Slytherin"
}];

let newStudent = {
name: "Cedric Diggory",
house: "Hufflepuff"
};

//TimeOut is added to have some delay
const addCharacter = (character) => {
setTimeout(() => {
students.push(character);
}, 1000);
}

const getListOfSortedStudents = (list) => {
list.forEach((student, index) => {
console.log(`${index + 1}. ${student.name} is added to ${student.house}`);
})
};

// This function will add a new student to the list,
// but the catch here is it will take 1 second
addCharacter(newStudent);

// This will console the list of the sorted student
getListOfSortedStudents(students);

The output will be:

So here in the above code, the addCharacter function is called with newStudent, and it adds newStudent to the students array using setTimeout with a delay of 1000 milliseconds (1 second). Meanwhile, the getListOfSortedStudents function is called with the current state of the students array(which still contains the initial value where there are only 2 students), and it prints the existing (2)students to the console. After the delay of 1000 milliseconds, the callback inside setTimeout is executed, and newStudent is added to the students array.

Now here asynchronous programming comes into the picture. In an ideal scenario getListOfSortedStudents should wait for the addCharacter function to complete its functionality and then getListOfSortedStudents do the desired tasks. Now let’s first achieve this using callback.

Here is the code the get the desired output using callback:

let students = [{
name: "Harry potter",
house: "Gryffindor"
}, {
name: "Draco Malfoy",
house: "Slytherin"
}];

let newStudent = {
name: "Cedric Diggory",
house: "Hufflepuff"
};

//TimeOut is added to have some delay
const addCharacter = (character, callback) => {
setTimeout(() => {
students.push(character);
callback(students);
}, 1000);
}

const getListOfSortedStudents = (list) => {
list.forEach((student, index) => {
console.log(`${index + 1}. ${student.name} is added to ${student.house}`);
})
};

addCharacter(newStudent, getListOfSortedStudents);

The output will be:

Explanation:

  1. The students array initially contains two objects representing Harry Potter and Draco Malfoy.
  2. The newStudent object representing Cedric Diggory is created.
  3. The addCharacter function is called with newStudent and the getListOfSortedStudents callback. It adds newStudent to the students array using setTimeout with a delay of 1000 milliseconds (1 second).
  4. Meanwhile, the getListOfSortedStudents function is not directly called in the main code; instead, it is passed as a callback to addCharacter.
  5. After the delay of 1000 milliseconds, the callback inside setTimeout is executed. It adds newStudent to the students array and then calls the provided callback (getListOfSortedStudents) with the updated students array.
  6. The getListOfSortedStudents function is called with the updated students array, and it prints all three students to the console.

The above example demonstrates the use of a callback to handle the result of an asynchronous operation. The callback is executed once the asynchronous operation (adding a character) is completed.

Now let’s implement the same code using promises.

let students = [{
name: "Harry potter",
house: "Gryffindor"
}, {
name: "Draco Malfoy",
house: "Slytherin"
}];

let newStudent = {
name: "Cedric Diggory",
house: "Hufflepuff"
};

//TimeOut is added to have some delay
const addCharacter = (character) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
students.push(character);
resolve(students);
} catch (e) {
reject(e);
}
}, 1000);
});
}

const getListOfSortedStudents = (list) => {
list.forEach((student, index) => {
console.log(`${index + 1}. ${student.name} is added to ${student.house}`);
})
};

addCharacter(newStudent).then((listOfStudents) => {
getListOfSortedStudents(listOfStudents);
}, (error) => {
console.log("Error", error);
});

Explanation:

  1. The addCharacter function is modified to return a Promise. The asynchronous operation (adding a character to the students array) is wrapped inside the Promise constructor. If the operation is successful, it resolves with the updated students array; if an error occurs, it rejects with the error.
  2. The addCharacter(newStudent) returns a Promise, and the .then() method is used to handle the resolved value (success) and rejected value (error).
  3. If the Promise is resolved, the success callback is executed, which calls getListOfSortedStudents with the updated list of students.
  4. If the Promise is rejected, the error callback is executed, logging the error message.

This usage of Promises provides a more structured way to handle asynchronous operations and makes the code easier to read and maintain. Promises are especially useful when dealing with multiple asynchronous operations or when chaining asynchronous calls.

Now let’s implement the same code using async/await.

let students = [{
name: "Harry potter",
house: "Gryffindor"
}, {
name: "Draco Malfoy",
house: "Slytherin"
}];

let newStudent = {
name: "Cedric Diggory",
house: "Hufflepuff"
};

//TimeOut is added to have some delay
const addCharacter = (character) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
student.push(character);
resolve(students);
} catch (e) {
reject(e);
}
}, 1000);
});
}

const getListOfSortedStudents = (list) => {
list.forEach((student, index) => {
console.log(`${index + 1}. ${student.name} is added to ${student.house}`);
})
};

(async function() {
try {
await addCharacter(newStudent);
getListOfSortedStudents(students);
} catch (e) {
console.log("Error", e);
}
})();

Explanation:

  1. A self-invoking function has been called with async keyword and under that function, we are calling addCharacter function with and getListOfSortedStudents in a sequence.
  2. Calls addCharacter with newStudent and waits for the promise to be resolved using await.
  3. After the promise is resolved, it calls getListOfSortedStudents to print the updated list of students.

The async and await keywords in JavaScript are used to work with asynchronous code and Promises in a more synchronous and readable manner. The async keyword is used to define a function as asynchronous. The await keyword is used inside an async function to wait for a Promise to resolve before proceeding with the execution of the code.

In summary, Asynchronous programming in JavaScript is crucial for handling concurrent tasks efficiently. Callbacks, promises, and async/await provide different approaches to managing asynchronous code, with each having its advantages. Promises and async/await are generally preferred for their readability and error-handling capabilities.

Thanks for Reading!!!

--

--

No responses yet