Callback and Promises
Callback Function
Definition
A callback is a function that you pass into another function as an argument. The function receiving the callback will call it (execute it) at some point in its process, either immediately, or after some event has occurred. Callbacks are widely used in JavaScript, especially for handling asynchronous operations like network requests, timers, event handling, and more.
Example
function greet(name) {
console.log('Hello, ' + name);
}
function processUserInput(callback) {
const name = prompt('Please enter your name.');
callback(name);
}
processUserInput(greet);
In this example:
greet
is the callback function that is passed toprocessUserInput
.The
processUserInput
function then calls thegreet
function, passing the user input (name
) as an argument.
Synchronous Callbacks vs Asynchronous Callbacks
Synchronous Callbacks
A synchronous callback is executed immediately, as part of the main program flow. That means it happens during the execution of the function that calls it.
Example:
function logMessage(message) {
console.log(message);
}
function process(callback) {
// do some synchronous task
callback('Task is done!');
}
process(logMessage);
In this case, the callback (logMessage
) is invoked immediately after the synchronous task is completed.
function getTwoNumbersAndAdd(number1, number2, onSuccess, onFailure) {
if (typeof number1 === "number" && typeof number2 === "number") {
onSuccess(number1, number2);
} else {
onFailure();
}
}
function addTwoNumbers(num1, num2) {
console.log(num1 + num2);
}
function onFail() {
console.log("Wrong data type");
console.log("please pass numbers only");
}
getTwoNumbersAndAdd(4, 4, addTwoNumbers, onFail);
Asynchronous Callbacks
An asynchronous callback is invoked after a certain task completes, usually after a delay, or after an asynchronous task such as fetching data or reading a file.
//Using SetTimeout
function logMessage() {
console.log('This message is displayed after 2 seconds');
}
setTimeout(logMessage, 2000); // Callback after 2000ms (2 seconds)
Here, setTimeout
is an asynchronous function that waits for 2 seconds, then calls logMessage
.
Asynchronous operations, such as making network requests, are a common use case for callbacks. Here's an example using a basic callback to simulate fetching data from an API.
//Using Asynchronous Example
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'Vitthal', age: 24 };
callback(data);
}, 1000); // Simulate 1-second delay for fetching data
}
function displayData(data) {
console.log(`Name: ${data.name}, Age: ${data.age}`);
}
fetchData(displayData);
In this case:
fetchData
is an asynchronous function that simulates fetching data from a server with a 1-second delay.Once the data is "fetched", it calls
displayData
, which is passed as a callback function tofetchData
.
Callback Hell
Callback Hell refers to a situation where multiple asynchronous operations depend on each other, leading to deeply nested callbacks. This makes the code hard to read, understand, and maintain.
Readability: The deeper the nesting, the harder it becomes to read and understand the flow.
Maintainability: If you need to add or change any logic, it becomes difficult to modify the code without breaking something.
Error Handling: Managing errors in such deeply nested structures is challenging, leading to complicated code to handle failure cases.
Pyramid of Doom
A Pyramid of Doom refers to deeply nested code, often seen in asynchronous programming where multiple callbacks are involved. The structure looks like a pyramid because of the increasing indentation level with each nested callback. This pattern makes code harder to read, debug, and maintain.
Promise
A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. When you create a promise, it can be in one of three states:
Pending: The initial state, neither fulfilled nor rejected.
Fulfilled: The operation completed successfully, and the promise now holds a result value.
Rejected: The operation failed, and the promise holds a reason for the failure (typically an error).
Syntax
let promise = new Promise(function(resolve, reject) {
// asynchronous task (e.g., fetching data)
// If success, call resolve(value)
// If failure, call reject(error)
});
resolve(value)
: This is called when the asynchronous operation completes successfully, and it moves the promise from the "pending" state to the "fulfilled" state. Thevalue
is passed to the next.then()
handler.reject(reason)
: This is called when the asynchronous operation fails, moving the promise to the "rejected" state. Thereason
(typically an error) is passed to the.catch()
handler.
Example
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
let success = true;
if (success) {
resolve("Operation Successful!");
} else {
reject("Operation Failed.");
}
}, 2000); // Simulating async work with a timeout
});
promise
.then(result => console.log(result)) // Handles fulfillment
.catch(error => console.log(error)); // Handles rejection
In this example, after 2 seconds, the promise will either resolve with the message "Operation Successful!"
or reject with the message "Operation Failed."
.
/* ------- produce --------*/
console.log("script start");
setTimeout(() => {
console.log("hello from setTimeout");
}, 0);
const bucket = ["coffee", "chips", "salt", "rice"];
const friedRicePromise = new Promise((resolve, reject) => {
if (
bucket.includes("vegetables") &&
bucket.includes("salt") &&
bucket.includes("rice")
) {
resolve({ value: "friedrice" });
} else {
reject("could not do it");
}
});
/* ------- Consume --------*/
friedRicePromise
.then(
// jab promise resolve hoga
(resolve) => {console.log("lets eat ", resolve)})
.catch((error) => {console.log(error)});
for (let i = 0; i <= 100; i++) {
console.log(Math.floor(Math.random(), i));
}
console.log("script end!!!!");
Function Returning a Promise
// function returning promise
function ricePromise() {
const bucket = ["coffee", "chips", "vegetables", "salts", "rice"];
return new Promise((resolve, reject) => {
if (
bucket.includes("vegetables") &&
bucket.includes("salt") &&
bucket.includes("rice")
) {
resolve({ value: "friedrice" });
} else {
reject("could not do it");
}
});
}
ricePromise()
.then(
// jab promise resolve hoga
(myfriedRice) => {
console.log("lets eat ", myfriedRice);
}
)
.catch((error) => {
console.log(error);
});
The Lifecycle of a Promise
When a promise is created, it starts in the pending state. Then, based on the outcome of the asynchronous task:
If the task completes successfully, the promise is resolved (fulfilled).
If the task encounters an error or fails, the promise is rejected.
Once a promise is either fulfilled or rejected, it becomes settled, meaning it won't change state anymore.
Promise Methods
then(onFulfilled, onRejected)
The
then
method takes two arguments: one for handling the successful resolution (onFulfilled
) and one for handling rejection (onRejected
).You can chain multiple
then
handlers.
Example:
promise.then(result => {
console.log(result); // "Operation Successful!"
});
catch(onRejected)
- The
catch
method is a shortcut to handle promise rejection. It's equivalent to.then(null, onRejected)
.
- The
Example:
promise.catch(error => {
console.error("Error:", error);
});
finally(callback)
- The
finally
method is executed once the promise is settled (whether fulfilled or rejected). It doesn't take any arguments and is typically used for cleanup actions.
- The
Example:
promise.finally(() => {
console.log("Operation finished, regardless of success or failure.");
});
Chaining Promises
One of the most powerful features of promises is chaining, which allows you to perform a sequence of asynchronous operations in a clean, readable manner.
let promise = new Promise((resolve, reject) => {
resolve(1);
});
promise
.then(result => {
console.log(result); // 1
return result * 2;
})
.then(result => {
console.log(result); // 2
return result * 3;
})
.then(result => {
console.log(result); // 6
});
Each then
returns a new promise, so you can chain multiple then
calls to handle a series of asynchronous actions.
function myPromise() {
return new Promise((resolve, reject) => {
const variable = true
if(!variable)resolve("foo");
else reject('no foo')
});
}
myPromise()
.then((value) => {
console.log(value);
value += "bar";
return value;
//return Promise.resolve(value)
})
.then((value) => {
console.log(value);
value += "baaz";
return value;
})
.then((value) => {
console.log(value);
})
.catch(value=>{
console.log(value);
})
Error Handling with Promises
Promises make it easier to manage errors in asynchronous code. If an error occurs in any part of a promise chain, the control is passed down to the nearest .catch()
.
new Promise((resolve, reject) => {
throw new Error("Oops!");
}).catch(error => {
console.log("Caught:", error); // "Caught: Error: Oops!"
});
You can also catch errors that occur in any part of the chain:
let promise = new Promise((resolve, reject) => {
resolve("Start");
});
promise
.then(result => {
console.log(result); // "Start"
throw new Error("Something went wrong");
})
.catch(error => {
console.log("Caught:", error);
// "Caught: Error: Something went wrong"
});
Callback Hell to Flat Code Using a Promise
Async / Await
Async/Await in JavaScript is a modern way to handle asynchronous operations, allowing you to write asynchronous code that looks synchronous, making it easier to read and maintain. It was introduced in ES8 (ES2017) and is built on top of promises, allowing you to avoid the complexity of using .then()
and .catch()
.
Key Concepts
Async Functions:
An
async
function is a function that returns a promise, and inside of it, you can use theawait
keyword.You define an async function by placing the
async
keyword before the function declaration.
Await:
await
pauses the execution of the function until the promise is resolved or rejected.It can only be used inside
async
functions.The result of an awaited promise is the value the promise resolves to.
Basic Structure
- Async function declaration:
async function myAsyncFunction() {
// Your async code here
}
Await:
- Use
await
to pause execution of the async function until a promise is resolved:
- Use
async function fetchData() {
let response = await fetch('https://api.example.com/data');
// Wait for fetch to complete
let data = await response.json();
// Wait for the response to convert to JSON
console.log(data);
// Now we can use the data
}
Error Handling:
- You can handle errors in async/await with
try
/catch
blocks.
- You can handle errors in async/await with
async function fetchData() {
try {
let response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
let data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
How Async/Await Works
Async Functions Always Return a Promise: An
async
function automatically wraps its return value in a promise. So, if you return a value from anasync
function, it is wrapped inPromise.resolve()
. If an error is thrown, it’s wrapped inPromise.reject()
.async function sayHello() { return "Hello"; // This is equivalent to: return Promise.resolve("Hello") } sayHello().then(result => console.log(result)); // Output: Hello
Await is Used to Unwrap a Promise: The
await
keyword makes JavaScript wait for a promise to settle (either resolved or rejected) and returns the result. If the promise is rejected, it throws the error, which you can handle withtry
/catch
.async function getResult() { let result = await Promise.resolve('Success!'); console.log(result); // Output: Success! }
Example: Simple API Request with Async/Await
Here’s a simple example using async/await
to fetch data from an API and handle errors.
async function getData() {
try {
let response =
await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
let data = await response.json();
console.log(data); // Data from the API
} catch (error) {
console.error('Error:', error); // Error handling
}
}
getData();
In the code above:
The
fetch
call is wrapped withawait
, so the function waits for the network request to complete before moving on.We check for network errors and use a
try
/catch
block to handle failures.
Handling Multiple Promises
You can use Promise.all()
with async/await
to wait for multiple asynchronous operations to complete in parallel.
async function fetchMultipleData() {
try {
let [response1, response2] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts/1'),
fetch('https://jsonplaceholder.typicode.com/posts/2')
]);
let data1 = await response1.json();
let data2 = await response2.json();
console.log(data1, data2);
} catch (error) {
console.error('Error:', error);
}
}
fetchMultipleData();
In this case, Promise.all
is used to run the fetch requests concurrently (i.e., at the same time), and the await
waits until both requests have completed.
Error Handling in Async/Await
Errors in async
functions behave similarly to errors in synchronous code. If a promise is rejected, it throws an error, which you can catch using a try
/catch
block.
async function getDataWithErrorHandling() {
try {
let response = await fetch('https://some-invalid-url.com');
let data = await response.json();
console.log(data);
} catch (error) {
console.error('Caught an error:', error);
}
}
getDataWithErrorHandling();
If an error occurs in the fetch
request, it will be caught and logged.
Async/Await vs. Promises
Before async/await
, we used to chain .then()
and .catch()
with promises. While this works well for small examples, it can quickly become unreadable with complex logic (known as "promise hell" or "callback hell").
Promise chaining:
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Async/await:
async function getData() {
try {
let response = await
fetch('https://jsonplaceholder.typicode.com/posts/1');
let data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
getData();
Both methods achieve the same thing, but async/await
tends to be cleaner and more readable, especially for complex workflows.
Using Async/Await with Functions
You can also use async/await
within other types of functions, like arrow functions:
const fetchData = async () => {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
};
fetchData().then(data => console.log(data));
Or inside an object method:
const myObject = {
async fetchData() {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
}
};
myObject.fetchData().then(data => console.log(data));
Parallel Execution vs Sequential Execution
When you want to perform independent asynchronous operations in parallel, you should avoid using await
directly on each call in sequence. Instead, use Promise.all()
to make them run concurrently:
Sequential execution (slower):
async function fetchSequential() {
let data1 = await fetch('https://jsonplaceholder.typicode.com/posts/1');
let data2 = await fetch('https://jsonplaceholder.typicode.com/posts/2');
// Waits for data1 to resolve before fetching data2
}
Parallel execution (faster):
async function fetchParallel() {
let [data1, data2] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts/1'),
fetch('https://jsonplaceholder.typicode.com/posts/2')
]);
// Both requests happen at the same time
}
GitHub Link : GitHub - JavaScript Mastery
Subscribe to my newsletter
Read articles from Vitthal Korvan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Vitthal Korvan
Vitthal Korvan
🚀 Hello, World! I'm Vitthal Korvan 🚀 As a passionate front-end web developer, I transform digital landscapes into captivating experiences. you'll find me exploring the intersection of technology and art, sipping on a cup of coffee, or contributing to the open-source community. Life is an adventure, and I bring that spirit to everything I do.