Understanding async code in Javascript in 2024
Callbacks: The Early Days
The foundation of asynchronous JavaScript was built upon callbacks. A callback is a function passed into another function, to be invoked when a long-running operation completes. Let's illustrate with a simple example:
function fetchData(callback) {
// Simulate a network request
setTimeout(() => {
const data = { message: 'Greetings from the server!' };
callback(data);
}, 2000); // Simulate a 2-second delay
}
fetchData((data) => {
console.log(data.message);
});
Promises: A Cleaner Approach
Promises brought much-needed structure to the asynchronous world. A promise represents the eventual result of an asynchronous operation. It exists in one of three states:
Pending: The operation is ongoing.
Fulfilled: The operation completed successfully.
Rejected: The operation failed.
Let's refactor our example using promises:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = { message: 'Greetings from the server!' };
resolve(data);
}, 2000);
});
}
fetchData()
.then((data) => console.log(data.message))
.catch((error) => console.error(error));
Async/Await: Syntactic Sugar
The async
and await
keywords brought a revolution, making promise-based asynchronous code appear almost synchronous. It's a layer on top of promises, providing a cleaner way to write code that looks like "wait here until this is done."
Let's streamline our example further:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error(error);
}
}
fetchData().then((data) => console.log(data.message));