RMRM Full Stack & AI Engineer · All guides · Roadmaps
Languages · guide

JavaScript Promises Explained

A Promise is a built-in JavaScript object that represents the eventual completion or failure of an asynchronous operation and its resulting value. Promises provide a cleaner, more manageable alternative to deeply nested callbacks, enabling readable async code flows.

What Is a Promise?

A Promise is an object that acts as a placeholder for a value that is not yet available but will be resolved in the future. It can exist in one of three states: pending (initial state), fulfilled (operation succeeded), or rejected (operation failed). Once a Promise settles into either fulfilled or rejected, its state is permanent and cannot change again.

Why Promises Matter

Before Promises, asynchronous code relied on nested callbacks, leading to deeply indented, hard-to-read structures known as 'callback hell.' Promises flatten this structure by chaining asynchronous steps sequentially using .then() and .catch() handlers. This makes error handling centralized and control flow significantly easier to reason about.

How Promises Work

You create a Promise by passing an executor function to the Promise constructor: new Promise((resolve, reject) => { ... }). Inside the executor, you call resolve(value) on success or reject(reason) on failure, which transitions the Promise out of the pending state. Consumers attach .then(onFulfilled) to handle success and .catch(onRejected) to handle errors, both of which themselves return new Promises, enabling chaining.

Promise Combinators

JavaScript provides static utility methods for coordinating multiple Promises. Promise.all([p1, p2]) resolves when all input Promises fulfill, or rejects as soon as any one rejects. Promise.allSettled waits for every Promise to settle regardless of outcome, while Promise.race resolves or rejects as soon as the first Promise settles, and Promise.any resolves with the first fulfilled value.

async/await: Syntactic Sugar Over Promises

The async/await syntax introduced in ES2017 is built entirely on top of Promises and does not replace them. Marking a function async makes it implicitly return a Promise, and await pauses execution inside that function until the awaited Promise settles. Under the hood, every await expression is equivalent to a .then() call, so understanding Promises is essential for debugging async/await code correctly.

Key Gotcha: Unhandled Rejections

A common mistake is forgetting to attach a .catch() handler or a second argument to .then(), leaving a rejected Promise unhandled. In modern Node.js and browsers, unhandled rejections trigger a warning or even crash the process. Always terminate every Promise chain with .catch() or wrap await calls in a try/catch block to ensure errors surface visibly rather than silently disappearing.

Go deeper with an AI tutor that teaches this in context — and quizzes you on it.
Open the app — free to start

© RM Full Stack & AI Engineer · All guides · Roadmaps · Open the app