Asynchronous programming is at the heart of Node.js, and Promises are its primary abstraction. A Promise represents the eventual completion or failure of an asynchronous operation and has three states: pending, fulfilled, or rejected. Once a Promise settles, it stays settled forever. Code attaches success handlers with .then(), error handlers with .catch(), and cleanup logic with .finally(), allowing complex asynchronous flows to be expressed as readable chains.
The async/await syntax, introduced in modern JavaScript, makes working with Promises feel synchronous. An async function always returns a Promise, and the await keyword pauses execution inside the function until the awaited Promise resolves. This makes error handling natural with try/catch blocks and produces code that is easier to read and reason about than long chains of .then() calls.
When working with multiple Promises, JavaScript provides several combinators. Promise.all() takes an array of Promises and resolves when all of them succeed, rejecting on the first failure; it is ideal for running independent async operations concurrently for better performance. Promise.allSettled() waits for every Promise to either fulfill or reject without short-circuiting, which is useful when you want to know the outcome of each operation. Promise.race() resolves or rejects as soon as the first Promise settles, and Promise.any() resolves with the first fulfillment but only rejects if every Promise rejects. For older callback-based APIs, util.promisify() converts a function following the error-first callback convention into one that returns a Promise, allowing seamless integration with modern async/await code.