Asynchronous JavaScript - A Practical Guide to Callbacks, Promises, and Async/Await
Have you ever struggled to navigate a website because it loaded slowly? This means that the website was not responsive.
To address such issues, one of the methods developers incorporate into their coding practice is asynchronous programming.
In this article, I will dive deep into the practical aspects of asynchronous Javascript (AJAX), exploring topics such as promises, callbacks, and async/await.
What is Asynchronous programming in Javascript?
Asynchronous programming in Javascript is a technology that enables a program to skip to another task while processing the previous task in the background without interfering with the display of a webpage.
By default, Javascript is single-threaded and follows the synchronous programming pattern, executing tasks sequentially, one code block at a time.
Therefore, when a certain function requires more time to complete, the next line of code will not commence processing until the former task concludes.
Unlike this model, asynchronous programming makes it possible for Javascript to advance to the next block of code while waiting for the response from the previous code.
For example, developers can request data from external APIs while other tasks run simultaneously. This, in turn, ensures that the user interface remains responsive.
Why is Asynchronous programming important?
Apart from improving the responsiveness of applications, asynchronous programming also enables better utilization of system resources.
While one part of the program waits for an operation to complete, other parts can continue executing. Consequently, this reduces the idle time of the CPU and maximizes resource usage.
Additionally, this programming model is crucial for building scalable systems, especially where a large number of tasks are involved.
Also, in web development, asynchronous Javascript(AJAX) allows web pages to load dynamically without requiring a full page reload. Thereby boosting user experience.
Lastly, asynchronous programming is best used in building real-time applications such as financial trading or gaming platforms where timely processing is critical.
JavaScript callbacks
In JavaScript, a callback is a function that is passed as an argument into another function and is executed after the completion of a particular operation.
Consider a scenario where you want an action to take place after a specific task is completed.
Instead of waiting for the operation to finish, you can introduce a callback function to execute once the task is done.
Callbacks are best used in event-driven programming, where functions are triggered in response to events like user actions.
Additionally, it promotes modularity and allows you to properly organize code in a readable format.
Asynchronous callback:
// Asynchronous function with a callback
function performTaskAsync(callback) {
console.log("Task started");
// Simulating an asynchronous operation using setTimeout
setTimeout(function () {
console.log("Task completed");
// Execute the callback function
callback();
}, 2000); // Simulating a 2-second delay
}
// Callback function
function callbackFunction() {
console.log("Callback executed!");
}
// Using the asynchronous callback
performTaskAsync(callbackFunction);
In this example, performTaskAsync
simulates an asynchronous task using setTimeout
. The callback function is executed after the asynchronous task is completed.
Common use cases of callbacks
Callbacks are commonly used in the following scenarios:
Event handling: this is a type of asynchronous programming that incorporates the callback function.
// Event listener with a callback document.getElementById("myButton").addEventListener("click", function () { console.log("Button clicked!"); });
AJAX requests: An AJAX request allows developers to send and receive data from a server asynchronously, without having to reload the entire web page. It also uses callbacks.
In traditional AJAX, the
XMLHttpRequest
object is used to make requests to the server. However, modern web development often uses thefetch
API, which provides a more flexible and powerful way to handle HTTP requests.// AJAX request with a callback function fetchData(url, callback) { // Simulating an AJAX call fetch(url) .then(response => response.json()) .then(data => callback(data)) .catch(error => console.error(error)); } // Callback function function handleData(data) { console.log("Data received:", data); } // Using the fetchData function fetchData("https://api.example.com/data", handleData);
Callback hell
While callbacks are powerful, they can lead to a phenomenon known as "callback hell" or "pyramid of doom." This occurs when multiple asynchronous operations are nested within one another, resulting in deeply indented and hard-to-read code.
getUserData((userData) => {
getAdditionalInfo(userData, (additionalInfo) => {
processInfo(userData, additionalInfo, (result) => {
// More nested callbacks...
});
});
});
To prevent a callback hell:
Break down your code into smaller manageable pieces.
Implement proper error handling within callbacks.
Define named functions to promote usability.
Use control flow libraries like promises.
Javascript promises
Promises are a way to handle asynchronous operations more elegantly, compared to callbacks. A promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value.
It addresses the callback hell issue by improving the flow of the code and also making the code more readable.
Creating and using promises
A promise can be created by using the Promise
constructor, which takes a function with the parameters resolve
and reject
.
The resolve function is called when the asynchronous operation is successful, and reject is called when there's an error.
//creating a promise
const myPromise = new Promise((resolve, reject) => {
// Simulating an asynchronous operation
setTimeout(() => {
const success = true;
if (success) {
resolve("Operation successful!");
} else {
reject("Oops! Something went wrong.");
}
}, 1000);
});
// Using the promise
myPromise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
});
Chaining promises
Promises allow chaining, making it easier to manage multiple asynchronous operations sequentially. This is achieved using the then
method, which returns a new promise.
const firstPromise = new Promise((resolve) => {
setTimeout(() => {
resolve("First operation completed");
}, 1000);
});
const secondPromise = (message) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`${message}, Second operation completed`);
}, 1000);
});
};
// Chaining promises
firstPromise
.then((result) => {
console.log(result);
return secondPromise(result);
})
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
});
Handling errors with promises
Errors in promises are handled using the catch
method, which allows you to centralize error handling for all promises in a chain.
const errorPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = false;
if (success) {
resolve("Operation successful!");
} else {
reject("Oops! Something went wrong.");
}
}, 1000);
});
errorPromise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error); // Will catch the error from the promise
});
Promises terminology
There are only two properties used for Javascript promises - statemyPromise.state
and resultmyPromise.result
.
The following are the common promises terminologies and their corresponding results:
Promise state | Promise result | Description |
Pending | undefined | When an asynchronous function linked to a promise has neither failed nor succeeded. |
Fulfilled | a result value | The asynchronous function has succeeded. |
Rejected | an error object | The asynchronous function has failed |
Async/Await
Async/await is a modern JavaScript feature that simplifies the handling of asynchronous code.
An asynchronous function is declared using the async
keyword. Then the await
keyword is used inside the async function to pause its execution until the operation is complete.
Async/await is often used together with promises to handle asynchronous operations more effectively.
When an asynchronous function is invoked, it returns a promise, allowing you to chain multiple async operations together.
async function getData() {
try {
// Asynchronous operation using await
const response = await fetch('https://api.example.com/data');
// Check if the response was successful
if (!response.ok) {
throw new Error('Failed to fetch data');
}
// Convert the response to JSON
const data = await response.json();
// Process the data
console.log(data);
} catch (error) {
console.error('Error:', error.message);
}
}
Conclusion
In conclusion, mastering asynchronous JavaScript as a developer is crucial for building modern, responsive web applications.
Additionally, understanding when to employ each asynchronous pattern can significantly enhance the efficiency and maintainability of your codebase.
Further reading and resources
Introduction to Asynchronous JavaScript - MDN web docs
How to use promises - MDN web docs
Promise - The modern JavaScript tutorial
What is a callback function in JavaScript - Simplilearn
Subscribe to my newsletter
Read articles from Mmek-Abasi Ekon directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mmek-Abasi Ekon
Mmek-Abasi Ekon
I am a passionate Cloud Architect who thrives on designing scalable, efficient, and secure cloud solutions. I love exploring how different cloud services work together to solve complex problems, and there’s nothing more satisfying than creating architecture that’s both innovative and reliable. My mission is to simplify cloud concepts for teams and stakeholders, ensuring everyone understands how the cloud can drive success.