Understanding Asynchronous Programming: Callbacks, Promises, and async/await


In modern JavaScript development, handling asynchronous operations is a core skill. Whether you're fetching data from an API, reading files, or setting timers, understanding how JavaScript manages non-blocking operations is essential. In this post, we'll dive into three key concepts: callbacks, promises, and async/await.
What Is Synchronous vs Asynchronous Code?
JavaScript is single-threaded, which means it can execute only one task at a time. In synchronous programming, tasks run one after another, and each task must complete before the next one begins and it runs in the specific order. This can become problematic when dealing with time-consuming tasks like network requests.
// Synchronous
console.log("Start");
console.log("Middle");
console.log("End");
// Output:
// Start
// Middle
// End
// Asynchronous
console.log("Start");
setTimeout(() => {
console.log("Middle");
}, 1000);
console.log("End");
// Output:
// Start
// End
// Middle
Asynchronous code allows the program to continue running while waiting for other operations to complete.
Callbacks – The OG Way
A callback is a function passed as an argument to another function. It's invoked after the parent function completes its operation.
function greet(name, callback) {
console.log("Hello, " + name);
callback();
}
function sayBye() {
console.log("Goodbye!");
}
greet("Alice", sayBye);
Callbacks work fine for simple tasks but can lead to "callback hell" when you have multiple nested asynchronous operations:
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doAnotherThing(newResult, function(finalResult) {
console.log(finalResult);
});
});
});
Promises – A Cleaner Alternative
Promises provide a more structured way to handle asynchronous operations. A promise represents a value that may be available now, later, or never.
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received");
}, 1000);
});
}
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));
A promise has three states:
Pending: Initial state
Fulfilled: Operation completed successfully
Rejected: Operation failed
async/await – Syntactic Sugar
Introduced in ES2017, async
and await
simplify working with promises and make asynchronous code look synchronous.
async function getData() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
}
getData();
This approach enhances code readability and makes error handling more intuitive with try/catch
.
When to Use What?
Callbacks: Still useful for simple tasks or when working with legacy code.
Promises: Ideal for chaining asynchronous operations.
async/await: Best for writing readable, modern asynchronous code.
Conclusion
Understanding how JavaScript handles asynchronous operations is crucial for modern development. Happy coding!
Subscribe to my newsletter
Read articles from Ashiya Amanulla directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
