Promises are a way to handle asynchronous operations in NodeJS, providing a cleaner and more readable approach compared to callbacks. They represent the eventual completion (or failure) of an asynchronous operation.
It acts as a placeholder for a result that is not immediately available but will be at some point in the future. Promises are used to manage asynchronous operations such as file reading, network requests, or database queries.
A Promise can be in one of three states
Pending: The initial state; the asynchronous operation is still in progress.
Fulfilled: The operation was completed successfully, and a result is available.
Rejected: The operation failed, and an error or reason is provided.
Syntax
new Promise((resolve, reject) => resolve("Success")).then(result => console.log(result));
The Promise is created with the resolve() function immediately resolving the value "Success".
The .then() method is used to handle the resolved value, logging it to the console.
How Do Promises Work in Node.js?
In NodeJS, asynchronous tasks such as reading files, making HTTP requests, or querying a database are handled without blocking the execution of other code. Traditionally, callbacks were used to handle these asynchronous tasks.
Callbacks can quickly lead to callback hell—a situation where callbacks are nested within callbacks, making the code difficult to read and maintain.
Promises help mitigate this issue by providing a more structured way to handle asynchronous operations. Instead of nesting functions, Promises allow chaining of operations, leading to cleaner and more understandable code.
Creating a Promise
A Promise is created using the new Promise() constructor. The constructor takes a single argument—a function known as the executor function—which is invoked immediately. The executor function takes two arguments:
resolve: A function to call if the asynchronous operation is successful.
reject: A function to call if the asynchronous operation fails.
Creating a Promise: We create a new Promise that simulates a task taking 2 seconds.
Resolving or Rejecting: After 2 seconds, the task either resolves with a success message or rejects with a failure message, based on the success variable.
Handling the Promise: We use .then() to handle a successful resolution and .catch() to handle any errors.
Output
Task completed successfully!
Simplified Error Handling
Error handling is important in asynchronous JavaScript. Promises provide a cleaner way to manage errors compared to callbacks. Using .catch() at the end of a promise chain simplifies error management.
The promise randomly resolves or rejects based on a generated number.
The .then() blocks handle successful resolutions.
The .catch() block handles any rejections that occur in the promise or any of the .then handlers.
Output (if an error occurs during the first fetch):
Promise chaining allows you to execute asynchronous operations sequentially. Each .then() receives the result of the previous one. This creates a readable and maintainable flow for asynchronous code.
Promises are chained using .then().
Each .then() receives the result from the previous promise.
Why Moved from Callbacks to Promises (Callback Hell Issue)
Callbacks were the original way to handle asynchronous operations in JavaScript. However, deeply nested callbacks can lead to "callback hell," making code difficult to read and maintain. Promises offer a cleaner and more structured approach.
Output
Final result: Result from 1 and Result from 2 and Result from 3
This code shows how nested callbacks make asynchronous code hard to read. Each step depends on the previous one, creating a "pyramid of doom."
Error handling is also complex, with repetitive if (err) checks at each level. This can become difficult to manage as the code grows.
The deep nesting makes it hard to visualize the overall flow of the asynchronous operations.
Same example with Promises
Promises offer a cleaner and more structured way to handle asynchronous operations, avoiding the "callback hell" of nested callbacks. They make asynchronous code easier to read, write, and maintain.
This code uses .then() chaining to sequence the asynchronous operations. Each .then() receives the result from the previous one.
The .catch() block at the end handles any errors that might occur in any of the promises, making error management much simpler.
Key Issues with Callbacks (Callback Hell)
Callback hell introduces significant problems for code readability and maintainability.
Inverted Control Flow: The logic for handling results is scattered within the nested callbacks, making it difficult to follow the overall flow of the program.
Error Handling Complexity: Error handling becomes cumbersome, with repetitive if (err) checks at each level of nesting. It's easy to miss handling errors in some branches
Nested Promises
In JavaScript, Promises are used to handle asynchronous operations, allowing you to attach callback functions that execute when the operation is completed or fails.
fetchData returns a Promise that resolves after 1 second.
processData takes the fetched data and returns a Promise that resolves after another second.
The .then() methods are chained to handle the results sequentially.
The .catch() method handles any errors that occur during the asynchronous operations.