A Promise is an object that represents the eventual result of an async operation. Instead of passing a callback, the async function returns a Promise — a container that will eventually hold the result (or an error).
Copy
const promise = fetch("http://localhost:8000/api/users");console.log(promise); // Promise { <pending> }// The data isn't here yet, but the Promise will deliver it when it arrives
A Promise has three states:
State
Meaning
Pending
The operation is still running
Fulfilled
The operation completed successfully (has a result)
Rejected
The operation failed (has an error)
Think of a Promise like a food delivery order. It starts as “pending” (being prepared). It either gets “fulfilled” (delivered successfully) or “rejected” (restaurant cancelled the order). You can’t change the outcome once it’s settled.
A single .catch() at the end handles errors from any step in the chain. If fetch() fails, or response.ok is false, or .json() fails — the error flows down to .catch().
Always add .catch() at the end of a Promise chain. Without it, errors are silently swallowed. You’ll see “Uncaught (in promise)” warnings in the console, but your code won’t handle them.
Promise.all() takes an array of Promises and returns a single Promise that resolves when all of them complete. If any one fails, the whole thing rejects.
Promises solved callback hell, but chaining .then() calls can still get verbose. The next step — async/await — lets you write the same async code in a way that reads like synchronous code:
async/await is built on top of Promises — it’s syntactic sugar, not a replacement. Understanding Promises helps you understand what async/await is doing under the hood.