Asynchronous JavaScript is one of the most important concepts to understand when building modern web applications. At first, asynchronous code is often written using callbacks. But when multiple asynchronous operations depend on each other, the code can become difficult to read and maintain. This problem is commonly known as callback hell. JavaScript Promises provide a cleaner way to handle asynchronous operations using .then(), .catch(), and .finally(). In this article, we'll understand: Callback hell What a Promise is Promise states: pending, fulfilled, and rejected .then() .catch() .finally() Promise chaining Error propagation Error recovery 1. What is a Callback? A callback is a function that is passed to another function and executed later, usually after an operation has completed. function getUser(callback) { setTimeout(() => { callback("User data"); }, 1000); } getUser((data) => { console.log(data); }); Enter fullscreen mode Exit fullscreen mode Here: getUser() starts an asynchronous operation. The callback function is passed to getUser(). After one second, the callback is executed. "User data" is received by the callback. Callbacks work well for simple operations, but problems appear when we have many dependent asynchronous operations. 2. Callback Hell Imagine that we need to: Get a user Get that user's posts Get comments for those posts Get likes for those comments Using callbacks, we might write: getUser((user) => { getPosts(user, (posts) => { getComments(posts, (comments) => { getLikes(comments, (likes) => { console.log(likes); }); }); }); }); Enter fullscreen mode Exit fullscreen mode Notice how the functions keep getting nested inside one another. This is called callback hell, also known as the Pyramid of Doom. Why is callback hell a problem? Deeply nested callbacks can make code: Difficult to read Difficult to debug Difficult to maintain Difficult to handle errors in Harder to extend when more asynchronous operations are added Promises provide a cleaner alternative. Instead of: getUser((user) => { getPosts(user, (posts) => { getComments(posts, (comments) => { getLikes(comments, (likes) => { console.log(likes); }); }); }); }); Enter fullscreen mode Exit fullscreen mode We can write: getUser() .then(getPosts) .then(getComments) .then(getLikes) .catch(handleError); Enter fullscreen mode Exit fullscreen mode The flow becomes much easier to follow. 3. What is a Promise? A Promise is an object that represents the eventual result of an asynchronous operation. A Promise can either: Complete successfully Fail For example: const promise = new Promise((resolve, reject) => { // asynchronous operation resolve("Success!"); }); Enter fullscreen mode Exit fullscreen mode A Promise receives two functions: resolve() --- used when the operation succeeds reject() --- used when the operation fails For example: const promise = new Promise((resolve, reject) => { const success = true; if (success) { resolve("Operation successful"); } else { reject("Operation failed"); } }); Enter fullscreen mode Exit fullscreen mode 4. Promise States Every Promise has three possible states: Pending | +----> Fulfilled | +----> Rejected Enter fullscreen mode Exit fullscreen mode 4.1 Pending A Promise starts in the pending state. It means the asynchronous operation has not completed yet. const promise = new Promise((resolve, reject) => { // Operation is still running }); Enter fullscreen mode Exit fullscreen mode At this point, the Promise is pending. 4.2 Fulfilled If the operation completes successfully, we call resolve(). const promise = new Promise((resolve, reject) => { resolve("Data received"); }); Enter fullscreen mode Exit fullscreen mode The Promise is now fulfilled. The successful result can be handled using .then(). 4.3 Rejected If something goes wrong, we call reject(). const promise = new Promise((resolve, reject) => { reject("Something went wrong"); }); Enter fullscreen mode Exit fullscreen mode The Promise is now rejected. The error can be handled using .catch(). Important: A Promise settles only once A Promise starts as pending and eventually becomes either fulfilled or rejected. Once it is settled, its state cannot be changed again. Fulfilled / Pending ---- \ Rejected Enter fullscreen mode Exit fullscreen mode For example: const promise = new Promise((resolve, reject) => { resolve("Success"); reject("Error"); }); Enter fullscreen mode Exit fullscreen mode The rejection will not change the result because the Promise has already been fulfilled. 5. .then() The .then() method is used to handle a successful Promise result. Promise.resolve("Hello") .then((result) => { console.log(result); }); Enter fullscreen mode Exit fullscreen mode Output: Hello Enter fullscreen mode Exit fullscreen mode The value passed to resolve() becomes the value received by .then(). const promise = new Promise((resolve) => { resolve("User data"); }); promise.then((data) => { console.log(data); }); Enter fullscreen mode Exit fullscreen mode 6. Promise Chaining One of the biggest advantages of Promises is chaining. We can attach multiple .then() calls: Promise.resolve(10) .then((num) => { return num * 2; }) .then((num) => { return num + 10; }) .then((result) => { console.log(result); }); Enter fullscreen mode Exit fullscreen mode The output is: 30 Enter fullscreen mode Exit fullscreen mode The flow is: 10 ↓ 10 × 2 ↓ 20 ↓ 20 + 10 ↓ 30 Enter fullscreen mode Exit fullscreen mode The important rule Whatever you return from one .then() becomes the input to the next .then(). For example: Promise.resolve(5) .then((value) => { return value * 2; }) .then((value) => { console.log(value); }); Enter fullscreen mode Exit fullscreen mode The first .then() returns 10, so the second .then() receives 10. 7. Chaining Asynchronous Operations Promises become especially useful when one asynchronous operation depends on another. For example: getUser() .then((user) => { return getPosts(user); }) .then((posts) => { return getComments(posts); }) .then((comments) => { console.log(comments); }) .catch((error) => { console.log(error); }); Enter fullscreen mode Exit fullscreen mode The flow is: getUser() ↓ user ↓ getPosts(user) ↓ posts ↓ getComments(posts) ↓ comments Enter fullscreen mode Exit fullscreen mode Each operation returns a Promise, and the next .then() waits for that Promise to settle successfully. 8. .catch() .catch() is used to handle rejected Promises and errors that occur in the Promise chain. Promise.reject("Network error") .catch((error) => { console.log(error); }); Enter fullscreen mode Exit fullscreen mode Output: Network error Enter fullscreen mode Exit fullscreen mode A common pattern is: fetchData() .then((data) => { console.log(data); }) .catch((error) => { console.log("Error:", error); }); Enter fullscreen mode Exit fullscreen mode If fetchData() fails, the .catch() handles the error. 9. Error Propagation This is one of the most important Promise concepts. Suppose an error occurs inside a .then(): Promise.resolve() .then(() => { throw new Error("Something went wrong"); }) .then(() => { console.log("This will not execute"); }) .catch((error) => { console.log(error.message); }); Enter fullscreen mode Exit fullscreen mode Output: Something went wrong Enter fullscreen mode Exit fullscreen mode What happened? .then() ↓ Error is thrown ↓ Promise becomes rejected ↓ Next .then() is skipped ↓ .catch() handles the error Enter fullscreen mode Exit fullscreen mode An error can therefore propagate through the Promise chain until a .catch() handler is found. 10. Error Propagation Through Multiple .then() Calls Consider: Promise.resolve("Start") .then((value) => { console.log(value); return "Step 1"; }) .then((value) => { console.log(value); throw new Error("Failed!"); }) .then(() => { console.log("This won't run"); }) .then(() => { console.log("This won't run either"); }) .catch((error) => { console.log(error.message); }); Enter fullscreen mode Exit fullscreen mode The output is: Start Step 1 Failed! Enter fullscreen mode Exit fullscreen mode Once the error is thrown, the remaining .then() handlers are skipped until .catch() is reached. This is why we can put a single .catch() at the end of a Promise chain to handle errors from multiple operations. 11. .finally() .finally() runs regardless of whether the Promise is fulfilled or rejected. fetchData() .then((data) => { console.log(data); }) .catch((error) => { console.log(error); }) .finally(() => { console.log("Operation finished"); }); Enter fullscreen mode Exit fullscreen mode The flow is: Success → .then() → .finally() Failure → .catch() → .finally() Enter fullscreen mode Exit fullscreen mode A common use case is a loading indicator. setLoading(true); fetchData() .then((data) => { displayData(data); }) .catch((error) => { displayError(error); }) .finally(() => { setLoading(false); }); Enter fullscreen mode Exit fullscreen mode Whether the request succeeds or fails, the loading indicator is turned off. 12. .catch() Can Recover From an Error An interesting feature of Promise chains is that .catch() can return a value. Promise.reject("Error") .catch((error) => { console.log(error); return "Recovered"; }) .then((value) => { console.log(value); }); Enter fullscreen mode Exit fullscreen mode Output: Error Recovered Enter fullscreen mode Exit fullscreen mode Why does the final .then() execute? Because the .catch() returned "Recovered". The flow is: Rejected ↓ .catch() ↓ return "Recovered" ↓ Fulfilled ↓ .then() Enter fullscreen mode Exit fullscreen mode So a .catch() can handle an error and allow the Promise chain to continue. 13. Complete Example Let's combine all the concepts. function getUser() { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ id: 1, name: "Koushik" }); }, 1000); }); } function getPosts(user) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(["Post 1", "Post 2"]); }, 1000); }); } getUser() .then((user) => { console.log("User:", user); return getPosts(user); }) .then((posts) => { console.log("Posts:", posts); }) .catch((error) => { console.log("Error:", error); }) .finally(() => { console.log("Finished"); }); Enter fullscreen mode Exit fullscreen mode The execution flow is approximately: getUser() ↓ Pending ↓ Fulfilled ↓ .then(user) ↓ getPosts(user) ↓ Pending ↓ Fulfilled ↓ .then(posts) ↓ .catch() if something fails ↓ .finally() Enter fullscreen mode Exit fullscreen mode 14. Callback Hell vs Promises Callback version getUser((user) => { getPosts(user, (posts) => { getComments(posts, (comments) => { getLikes(comments, (likes) => { console.log(likes); }); }); }); }); Enter fullscreen mode Exit fullscreen mode Promise version getUser() .then((user) => getPosts(user)) .then((posts) => getComments(posts)) .then((comments) => getLikes(comments)) .then((likes) => { console.log(likes); }) .catch((error) => { console.log(error); }); Enter fullscreen mode Exit fullscreen mode The Promise version is flatter, easier to read, and provides a centralized error-handling mechanism. 15. Quick Summary Concept Meaning Callback Function executed later Callback Hell Deeply nested callbacks Promise Represents the eventual result of an async operation Pending Operation is still in progress Fulfilled Operation completed successfully Rejected Operation failed .then() Handles successful results .catch() Handles errors .finally() Runs regardless of success or failure Promise chaining Connecting multiple asynchronous operations Error propagation Errors move through the chain until handled The most important flow to remember is: ┌──→ Fulfilled → .then() │ Pending ─────┤ │ └──→ Rejected → .catch() ↓ .finally() Enter fullscreen mode Exit fullscreen mode And the most important Promise chaining rule is: The value returned from one .then() becomes the input to the next .then(). Final Takeaway Callbacks are useful for handling asynchronous operations, but deeply nested callbacks can lead to callback hell. Promises provide a structured way to manage asynchronous operations. A Promise starts as pending and eventually becomes either fulfilled or rejected. Use .then() for successful results. Use .catch() for errors. Use .finally() for code that should run regardless of the result. Use chaining to organize multiple asynchronous operations. Errors thrown anywhere in a chain can propagate to .catch(). Understanding these concepts gives you the foundation for learning async/await, which provides another cleaner way to work with Promises.
JavaScript Promises Explained: Callback Hell, Promise States, Chaining & Error Propagation
Full Article
Original Source
Read the full article at Dev →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.