NiketTongare

Updated at 25 May 2024

The trick is to bypass callback and promise hell.

Discover strategies to navigate and simplify asynchronous programming in JavaScript. Learn how to avoid callback and promise hell, making your code cleaner and more maintainable.

Javascript

56 views

The trick is to bypass callback and promise hell.

What is a Callback ?

A callback is a function that is passed as an argument to another function and is executed after the completion of that function. Callbacks are commonly used in asynchronous programming, where certain operations (like reading a file or making an API request) take time to complete, and we need to specify what should happen once the operation is done.

Here’s a simple example of a callback function:

function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: 'John Doe' };
callback(data);
}, 1000);
}

function displayData(data) {
console.log('Data received:', data);
}

fetchData(displayData);

In this example, fetchData takes a callback function (displayData) that is executed after the data is fetched (simulated with setTimeout).

What is Callback Hell ?

Callback hell refers to a situation where callbacks are nested within other callbacks many levels deep, making the code hard to read and maintain. This often occurs when dealing with multiple asynchronous operations that depend on each other.

Here’s an example of callback hell:

function getData(callback) {
setTimeout(() => {
const data = 'data1';
console.log('getData completed:', data);
callback(data);
}, 1000);
}

function getMoreData(data, callback) {
setTimeout(() => {
const moreData = data + ' more data';
console.log('getMoreData completed:', moreData);
callback(moreData);
}, 1000);
}

function getEvenMoreData(data, callback) {
setTimeout(() => {
const evenMoreData = data + ' even more data';
console.log('getEvenMoreData completed:', evenMoreData);
callback(evenMoreData);
}, 1000);
}

// Callback hell example
getData(function(x) {
getMoreData(x, function(y) {
getEvenMoreData(y, function(z) {
console.log('Final result:', z);
// Further nesting would continue here if there were more asynchronous steps
});
});
});

In this example, each function depends on the completion of the previous one, leading to nested callbacks and making the code difficult to follow.

Avoiding Callback Hell

  • Using Promises:
  • Using async/await:

What is a Promise ?

A Promise in JavaScript is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It provides a cleaner, more structured way to handle asynchronous operations compared to callbacks.

A Promise has three states:

  • Pending: The initial state, neither fulfilled nor rejected.
  • Fulfilled: The operation completed successfully.
  • Rejected: The operation failed.

Here’s an example of using a Promise:

function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = { id: 1, name: 'John Doe' };
resolve(data); // Or reject('Error occurred') for error case
}, 1000);
});
}

fetchData()
.then(data => {
console.log('Data received:', data);
})
.catch(error => {
console.error('Error:', error);
});
If we attempt to avoid callback hell using promises, we may encounter a new problem known as promise hell.

Promise Hell

Promise hell occurs when promises are chained in a complex and unmanageable way, similar to callback hell, making the code difficult to read and maintain. This usually happens when multiple asynchronous operations depend on each other.

Here’s an example of promise hell:

function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('getData completed');
resolve('data1');
}, 1000);
});
}

function getMoreData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('getMoreData completed:', data);
resolve(data + ' more data');
}, 1000);
});
}

function getEvenMoreData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('getEvenMoreData completed:', data);
resolve(data + ' even more data');
}, 1000);
});
}

// Promise hell example
getData()
.then(data => {
return getMoreData(data);
})
.then(moreData => {
return getEvenMoreData(moreData);
})
.then(evenMoreData => {
console.log('Final result:', evenMoreData);
// Further chaining would continue here if there were more asynchronous steps
})
.catch(error => {
console.error('Error:', error);
});

While this example is more readable than deeply nested callbacks, it can still become unwieldy as the number of chained promises increases.

Avoiding Promise Hell

  • Using async/await:

What is async/await ?

async and await are keywords in JavaScript that provide a more concise and readable way to work with asynchronous code, allowing developers to write asynchronous code that looks and behaves like synchronous code.

async Function

An async function is a function that returns a Promise. It allows you to use the await keyword within its body. Declaring a function as async ensures that it automatically returns a Promise, and makes it easier to read and write asynchronous code.

await Expression

The await expression pauses the execution of an async function until the Promise it is waiting for is resolved or rejected. When the Promise is resolved, await returns the resolved value. If the Promise is rejected, await throws the rejected value, which can be caught using try...catch.

Here’s a simple example demonstrating how async and await work:

// Using async/await to handle the Promise
async function getData() {
try {
const data = await fetchData(); // Await pauses until fetchData is resolved
console.log('Data received:', data);
} catch (error) {
console.error('Error:', error); // Catches any errors from fetchData
}
}

getData();

Benefits of async/await

  • Readability: async/await makes asynchronous code look more like synchronous code, which improves readability and maintainability.
  • Error Handling: Using try...catch blocks with async/await makes error handling more straightforward compared to chaining .catch in Promises.
  • Flattened Code Structure: async/await helps avoid deeply nested structures, commonly known as "callback hell" or "promise hell."
JavaScript is by default a synchronous

Problem with async/await

Consider the following code:

async function getData() {
try {
const data = await fetchData();
const data1 = await fetchData2();
const data2 = await fetchData3();
const data3 = await fetchData4();
} catch (error) {
console.error('Error:', error); // Catches any errors
}
}

getData();

In this example, if an error occurs, the catch block will log it. However, this approach doesn't provide information about which fetchData function caused the error.

To address this, you might consider nesting try...catch blocks for each asynchronous operation:

async function getData() {
try {
const data = await fetchData();
} catch (error) {
console.error('Error in fetchData:', error);
return;
}

try {
const data1 = await fetchData2();
} catch (error) {
console.error('Error in fetchData2:', error);
return;
}

try {
const data2 = await fetchData3();
} catch (error) {
console.error('Error in fetchData3:', error);
return;
}

try {
const data3 = await fetchData4();
} catch (error) {
console.error('Error in fetchData4:', error);
return;
}
}

getData();

While this solution provides more detailed error information, it introduces "try...catch" hell, making the code cumbersome and harder to maintain. Each asynchronous call requires its own try...catch block, leading to verbose and repetitive code.

What's the perfect solution ?

To avoid "try...catch" hell while retaining detailed error information for each asynchronous operation, we can implement a custom solution using promises. We created a function called safePromise, as shown below:

const safePromise = (promise) => 
promise
.then((data) => [null, data])
.catch((error) => [error]);

The safePromise function wraps a promise and ensures that it always returns an array with two elements. The first element is the error (if any), and the second element is the result of the promise.

Here's how you can use safePromise to handle errors more cleanly:

const [error, result] = await safePromise(fetchData(...));

if(error) {
// handle error
}

const [error1, result1] = await safePromise(fetchData1(...));

if(error1) {
// handle error
}

With safePromise, you can avoid nesting multiple try...catch blocks and handle errors for each asynchronous operation in a more concise and readable manner. This approach simplifies error handling and maintains the readability of your code.

Author: Niket Tongare

#the
#trick
#is
#to
#bypass
#callback
#and
#promise
#hell
#discover
#strategies
#navigate
#simplify
#asynchronous
#programming
#in
#javascript
#learn
#how
#avoid
#making
#your
#code
#cleaner
#more
#maintainable

Made with love by

@nikettongare