A Guide to Async JavaScript: Mastering Callbacks and Promises
As a fellow geek, I'm excited to share my insights into Asynchronous JavaScript with other enthusiasts and aspiring JavaScript beginners. In this blog, we'll delve into everything from the fundamentals to advanced concepts of asynchronous JavaScript, including callbacks and promises. This journey will deepen your understanding and transform your learning into lasting knowledge
Before we get in to the Aysnc Js ,we have to see what is synchronous Javascript then we will get clear picture of both of them
Synchronous JavaScript
By default, JavaScript runs in a synchronous, single-threaded way, meaning it processes code one line at a time from top to bottom. It waits for each line to finish before starting the next one. This makes the code run in a sequence, which is predictable but can be inefficient for tasks that take time, like network requests or file operations.
// example for synchronous code
console.log("hi geek")
console.log("welcome to my blog")
function welcome(name){
console.log("come in ",name)
}
function farewell(name) {
logMessage(`goodbye, ${name}`);
}
welcome("ganesh")
farewell("ganesh")
//hi geek
//welcome to my blog
//come in ganesh
//goodbye, ganesh
The first line
console.log("hi geek");
outputs "hi geek" to the console.The second line
console.log("welcome to my blog");
outputs "welcome to my blog" to the console.The
welcome
function is called with the argument "ganesh", which outputs "come in ganesh" to the console.The
farewell
function is called with the argument "ganesh", which outputs "goodbye, ganesh" to the console.
Each line of code is executed in order, demonstrating the synchronous nature of JavaScript, where each operation completes before the next one begins.
Asynchronous Javascript
Asynchronous JavaScript means the code doesn't run line by line in order. It lets the code continue without waiting for previous tasks to finish. This non-blocking nature is useful for time-consuming tasks like network requests or file operations, keeping the program responsive and efficient. Asynchronous behavior is usually handled with callbacks, promises, or async/await syntax.
console.log("going to download");
function fetchData() {
setTimeout(() => {
console.log("downloaded");
}, 2000);
}
fetchData();
console.log("watching the downloaded movie");
Output:
going to download
watching the downloaded movie
downloaded
console.log("going to download");
- This line logs "going to download" to the console immediately.The
fetchData
function is defined. Inside this function,setTimeout
is used to simulate an asynchronous operation. It schedules the function to log "downloaded" to the console after a 2000-millisecond (2-second) delay.fetchData();
- This line calls thefetchData
function, initiating the asynchronous operation.console.log("watching the downloaded movie");
- This line logs "watching the downloaded movie" to the console immediately after thefetchData
function is called, without waiting for the 2-second delay to complete.After the 2-second delay, the message "downloaded" is logged to the console.
This example demonstrates asynchronous behavior, where the setTimeout
function allows the rest of the code to continue executing while waiting for the specified delay to complete.
Now, I believe we have a basic understanding of async JavaScript, so we can dive into the topics.
We will learn about these in our blog.
What is a callback in JavaScript
How callbacks work and their use cases
Disadvantages of callbacks
How can we solve issues
What is a promise in JavaScript
A promise comes as a solution
Consume a promise and create a promise
Promise chaining
Promise API
Let's dive into the topics and explore the depths of callbacks and promises in the JavaScript ocean. We'll start with understanding what callbacks are, how they work, and their advantages and disadvantages. Then, we'll move on to promises, how they provide solutions to callback issues, and how to effectively use them, including promise chaining and the Promise API. This comprehensive journey will enhance your understanding and mastery of asynchronous programming in JavaScript.
Synchronous JavaScript
By default, JavaScript is a synchronous, single-threaded language, meaning the JavaScript engine executes code line by line from top to bottom. While executing, it waits for the current line of code to complete before moving to the next line. This ensures that the code runs in a specific sequence, which can be predictable but may lead to inefficiencies when dealing with tasks that take time, such as network requests or file operations.
// example for synchronous code
console.log("hi geek")
console.log("welcome to my blog")
function welcome(name){
console.log("come in ",name)
}
function farewell(name) {
logMessage(`goodbye, ${name}`);
}
welcome("ganesh")
farewell("ganesh")
//hi geek
//welcome to my blog
//come in ganesh
//goodbye, ganesh
The first line
console.log("hi geek");
outputs "hi geek" to the console.The second line
console.log("welcome to my blog");
outputs "welcome to my blog" to the console.The
welcome
function is called with the argument "ganesh", which outputs "come in ganesh" to the console.The
farewell
function is called with the argument "ganesh", which outputs "goodbye, ganesh" to the console.
Each line of code is executed in order, demonstrating the synchronous nature of JavaScript, where each operation completes before the next one begins.
What is a callback in JavaScript
A callback is a function that is passed as an argument to another function and is executed after the outer function has completed its operation
// Sample callback function
function main(number1, number2, callback) {
console.log("hi iam main function")
let result = callback(number1, number2);
console.log(result);
}
function add(number1, number2) {
let addedNumber = number1 + number2;
return addedNumber;
}
main(3, 4, add); // This will log 7 to the console
The
main
function has three parameters:number1
,number2
, andcallback
. It logs the message "hi iam main function" and then logs the result obtained from the callback function.The callback function
add
takes the values from the parameters, adds them, and returns the sum.When we call the
main
function with the arguments3
,4
, andadd
, it executes the code within themain
function, logs the message "hi iam main function," then calls theadd
function as the callback. The result is obtained and logged.This demonstrates how we can control the sequence of execution by running the
add
function after themain
function.
How callbacks work and their use cases
How Callbacks Work:
Passing Functions as Arguments: A callback function is passed as an argument to another function. This allows the outer function to execute the callback at a specific point in its execution.
Execution After Completion: The callback is typically executed after the outer function has completed its main task, making it ideal for operations that take time, such as data fetching or file reading.
Use Cases for Callbacks:
Asynchronous Programming: Callbacks are essential for managing asynchronous tasks like API calls, where you want to perform an action once the data is retrieved.
function dataFetching(callback){ let fetchedData = fetch("www.exampleapi.com") callback(fetchedData) } function processData(data){ console.log("processing data") }
Event Handling: In event-driven programming, callbacks are used to define what should happen when an event occurs, such as a user clicking a button.
we can use the callback function to perform actions based on events like above
Timers: Functions like
setTimeout
andsetInterval
use callbacks to execute code after a specified delay or at regular intervals.function timer(){ setTimeout(() => { console.log("this callback executes after 2 seconds") }, 2000); } timer()
logs message
this callback executes after 2 seconds
after 2 seconds by using callback functionArray Methods: Methods like
map
,filter
, andforEach
use callbacks to apply a function to each element of an array, allowing for concise and expressive data manipulation.let sampleArray = [1, 2, 3, 4, 5]; let mutliplyTwo = sampleArray.map((number) => { return number * 2; }); console.log(mutliplyTwo); //output [ 2, 4, 6, 8, 10 ]
By using callbacks, JavaScript can efficiently handle operations that would otherwise block the execution of code, enhancing performance and responsiveness.
// Simulate an asynchronous operation using setTimeout
function fetchData(callback) {
console.log("Fetching data...");
// Simulate a delay of 2 seconds
setTimeout(() => {
const data = "Sample data";
callback(data);
}, 2000);
}
// Define a callback function to handle the fetched data
function processData(data) {
console.log(`Processing: ${data}`);
}
// Call fetchData with processData as the callback
fetchData(processData);
The
fetchData
function simulates an asynchronous operation usingsetTimeout
. It logs "Fetching data..." to the console and then waits for 2 seconds before executing the callback function.The
processData
function is a callback that takes the fetched data as an argument and logs it to the console.When
fetchData
is called withprocessData
as the callback, it simulates fetching data and then processes it after the delay, demonstrating how asynchronous callbacks work. This pattern is commonly used in JavaScript for handling operations like network requests, where you need to wait for a response before proceeding.
Disadvantages of Callbacks
Callback Hell: When multiple asynchronous operations are nested, it can lead to "callback hell," where the code becomes difficult to read and maintain due to deep nesting and complex logic.
function firstTask(callback) { setTimeout(() => { console.log("First task complete"); callback(); }, 1000); } function secondTask(callback) { setTimeout(() => { console.log("Second task complete"); callback(); }, 1000); } function thirdTask(callback) { setTimeout(() => { console.log("Third task complete"); callback(); }, 1000); } // Nested callbacks leading to callback hell firstTask(() => { secondTask(() => { thirdTask(() => { console.log("All tasks complete"); }); }); });
Error Handling: Managing errors in callback-based code can be challenging, as you need to ensure that errors are properly propagated and handled at each level of the callback chain.
Inversion of Control: Callbacks can lead to an inversion of control, where the flow of the program is determined by external functions, making it harder to follow the logic and debug.
// Function that simulates an asynchronous operation function fetchData(url, callback) { console.log(`Fetching data from ${url}...`); // Simulate a delay with setTimeout setTimeout(() => { const data = `Data from ${url}`; // Call the callback function with the fetched data callback(data); }, 2000); } // Callback function to process the data function processData(data) { console.log("Processing:", data); } // Inversion of control: fetchData decides when to call processData fetchData("https://api.example.com", processData);
Complexity: As the number of callbacks increases, the complexity of the code can grow, making it harder to understand and maintain, especially for developers new to the codebase.
How can we solve Issues
Promises: Promises offer a cleaner and more manageable way to handle asynchronous operations. They allow you to chain operations and handle errors more gracefully.
Async/Await: Built on top of promises, async/await syntax makes asynchronous code look and behave more like synchronous code, improving readability and maintainability.
Modular Code: Breaking down complex operations into smaller, reusable functions can help manage the complexity of nested callbacks.
Error-First Callbacks: Using error-first callback patterns ensures that errors are handled consistently and propagated correctly through the callback chain.
function fetchData(callback) { // Simulate an asynchronous operation using setTimeout setTimeout(() => { const error = null; // or new Error('Something went wrong'); const data = { id: 1, name: 'Sample Data' }; // Call the callback with error as the first argument callback(error, data); }, 1000); } function processData(error, data) { if (error) { console.error('Error:', error); return; } console.log('Data:', data); } // Call fetchData with processData as the callback fetchData(processData);
What is a Promise in JavaScript
A Promise is an object represents the eventual completion or failure of asynchronous operation
States of a Promise:
Pending: The initial state, neither fulfilled nor rejected.
Fulfilled: The operation completed successfully, and the promise has a resulting value.
Rejected: The operation failed, and the promise has a reason for the failure.
Methods:
then(): Used to specify what to do when the promise is fulfilled.
catch(): Used to handle errors or rejections.
finally(): Executes code after the promise is settled, regardless of its outcome.
Promises allow for chaining, where you can perform a series of asynchronous operations in sequence, making the code more readable and easier to manage.
A Promise Comes as a Solution
Promises provide a more structured and manageable way to handle asynchronous operations compared to callbacks. They help avoid callback hell by allowing you to chain operations and handle errors more gracefully.
Consume a Promise and Create a Promise:
Creating a Promise: You create a promise using the
Promise
constructor, which takes a function with two parameters:resolve
andreject
. You callresolve
when the operation is successful andreject
when it fails.const myPromise = new Promise((resolve, reject) => { const success = true; // Simulate success or failure if (success) { resolve("Operation successful"); } else { reject("Operation failed"); } });
Consuming a Promise: You consume a promise using the
then
andcatch
methods.then
is called when the promise is resolved, andcatch
is called when it is rejected.myPromise .then((message) => { console.log(message); }) .catch((error) => { console.error(error); });
Promise Chaining
Promise chaining allows you to perform a series of asynchronous operations in sequence. Each then
method returns a new promise, allowing you to chain multiple then
calls.
myPromise
.then((message) => {
console.log(message);
return "Next operation";
})
.then((nextMessage) => {
console.log(nextMessage);
})
.catch((error) => {
console.error(error);
})
.finally(()=>{
console.log("whatever happens i will execute after settled")
})
In this example, the second then
is executed after the first one, allowing for sequential operations.
Promise API
The Promise API provides several static methods to work with promises:
Promise.all(): Takes an array of promises and returns a single promise that resolves when all the promises have resolved or rejects if any promise is rejected.
Promise.all([promise1, promise2]) .then((results) => { console.log(results); }) .catch((error) => { console.error(error); });
Promise.race(): Returns a promise that resolves or rejects as soon as one of the promises in the array resolves or rejects.
const promise1 = new Promise((resolve, reject) => { setTimeout(resolve, 500, 'First'); }); const promise2 = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'Second'); }); Promise.race([promise1, promise2]).then((value) => { console.log(value); // Output: 'Second' });
Promise.allSettled(): Returns a promise that resolves after all of the given promises have either resolved or rejected, with an array of objects describing the outcome of each promise.
const promise1 = Promise.resolve('Resolved'); const promise2 = Promise.reject('Rejected'); const promise3 = Promise.resolve('Another Resolved'); Promise.allSettled([promise1, promise2, promise3]).then((results) => { console.log(results); // Output: [ // { status: 'fulfilled', value: 'Resolved' }, // { status: 'rejected', reason: 'Rejected' }, // { status: 'fulfilled', value: 'Another Resolved' } // ] });
Promise.any(): Returns a promise that resolves as soon as any of the promises in the array resolves, or rejects if all of them reject.
const promise1 = Promise.reject('Error 1'); const promise2 = Promise.reject('Error 2'); const promise3 = Promise.resolve('Success'); Promise.any([promise1, promise2, promise3]).then((value) => { console.log(value); // Output: 'Success' }).catch((error) => { console.error(error); });
Here's a comparison table for callbacks and promises
| Feature | Callbacks | Promises | | --- | --- | --- | | Syntax | Function passed as an argument | Object with
then
,catch
, andfinally
methods | | Readability | Can lead to callback hell with nested functions | More readable with chaining | | Error Handling | Requires manual error handling | Built-in error handling withcatch
| | Control Flow | Inversion of control | More structured control flow | | Flexibility | Less flexible, harder to manage | More flexible, easier to manage | | Asynchronous Handling | Basic asynchronous handling | Advanced asynchronous handling with chaining |Conclusion:
Callbacks are useful for simple asynchronous tasks but can lead to complex code, while promises offer a more readable, structured, and flexible approach with built-in error handling, making them preferable for complex operations in JavaScript.
Subscribe to my newsletter
Read articles from ganesh v directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by