JavaScript Promises: Complete Beginner Guide

Promises are the foundation of async JavaScript. Learn what a promise is, the three promise states, how .then() and .catch() work, and how to create your own promises.

7 min read

A promise is a JavaScript object that represents a value that does not exist yet but will exist later. Think of it like a ticket at a restaurant: you order food, you get a buzzer, and when the buzzer goes off, your food is ready.

The buzzer is the promise. The food is the value.

Before promises, async JavaScript used callbacks: functions passed to other functions that run when the work is done. Callbacks work, but they lead to deeply nested code called "callback hell." Promises flatten that nesting and give you a standard way to handle both success and failure.

javascriptjavascript
// A promise that resolves after 1 second
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Here is your data!");
  }, 1000);
});
 
myPromise.then((value) => {
  console.log(value);
});
 
console.log("Waiting...");

Notice the order: "Waiting..." logs immediately.

The promise's value only logs a full second later, once the timer inside the executor fires and calls resolve.

texttext
Waiting...
(after 1 second)
Here is your data!

The promise is created, a one-second timer starts, and the code after it runs immediately. When the timer fires, the callback in .then() runs.

The three promise states

Every promise is in exactly one of three states.

Promise states and transitions
StateMeaningTriggered by
PendingThe work is not done yetInitial state when the promise is created
FulfilledThe work succeededresolve(value) was called
RejectedThe work failedreject(error) was called

A promise starts pending. It can move to fulfilled or rejected, but never both. Once it settles, it stays settled forever.

You can call then on a settled promise and it runs immediately with the stored value.

javascriptjavascript
const resolved = Promise.resolve("already done");
 
resolved.then((value) => {
  console.log(value); // "already done" (runs immediately)
});

Creating a promise with new Promise()

You create a promise by passing a function (the executor) to new Promise(). The executor receives two functions, resolve and reject: call resolve with a value when the work succeeds, and call reject with an error when it fails.

javascriptjavascript
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    // Simulate an API call
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: "Taylor" });
      } else {
        reject(new Error("Invalid user ID"));
      }
    }, 500);
  });
}

The executor runs immediately when the promise is created. The setTimeout call simulates a network delay. After 500ms, the promise either resolves with a user object or rejects with an error.

javascriptjavascript
fetchUser(1)
  .then((user) => console.log("Got user:", user.name))
  .catch((err) => console.error("Failed:", err.message));
 
fetchUser(-1)
  .then((user) => console.log("Got user:", user.name))
  .catch((err) => console.error("Failed:", err.message));

The valid ID resolves and logs the user's name. The invalid ID rejects, so its own catch handler runs instead, without throwing an unhandled error.

texttext
Got user: Taylor
Failed: Invalid user ID

Each call to fetchUser creates a new promise. Promises are one-time; they settle once and never change.

Consuming promises with .then() and .catch()

You do not need to create promises to use them. Most of the time, you consume promises returned by APIs like fetch.

javascriptjavascript
fetch("https://api.example.com/users/1")
  .then((response) => response.json())
  .then((user) => console.log("User:", user.name))
  .catch((error) => console.error("Something went wrong:", error.message))
  .finally(() => console.log("Request finished (success or failure)"));

.then() runs when the promise fulfills. .catch() runs when it rejects. .finally() runs in both cases, for cleanup.

A key detail: then always returns a new promise. This is what makes chaining possible. The value you return from a then callback becomes the resolved value of the next promise in the chain.

Promise.resolve() and Promise.reject()

These are shortcuts for creating already-settled promises. They are useful for testing and for functions that need to return a promise but already have the value.

javascriptjavascript
// Create a fulfilled promise
const ready = Promise.resolve("Done");
 
// Create a rejected promise
const failed = Promise.reject(new Error("Something broke"));
 
ready.then(console.log); // "Done"
failed.catch((err) => console.error(err.message)); // "Something broke"

Promise.resolve() also converts thenables (objects with a then method) into standard promises. This is how most promise libraries interoperate.

Promises are microtasks

Promise callbacks run as microtasks, which execute before macrotasks like setTimeout.

javascriptjavascript
console.log("Start");
 
setTimeout(() => console.log("Timeout"), 0);
 
Promise.resolve().then(() => console.log("Promise"));
 
console.log("End");

The synchronous lines run first, in order: "Start" then "End". Only after the main script finishes does JavaScript drain the microtask queue, running the promise callback, before finally reaching the timer.

texttext
Start
End
Promise
Timeout

The promise callback runs before the timeout callback, even though both are deferred. Microtasks have higher priority than macrotasks. This matters when you mix promises with timers or event handlers.

Common promise mistakes for beginners

Forgetting that .then() returns a new promise. This is why chaining works and why forgetting to return a value inside a then callback breaks the chain.

javascriptjavascript
// Mistake: not returning from .then()
fetch("/users/1")
  .then((res) => {
    res.json(); // No return! The next .then() gets undefined
  })
  .then((data) => {
    console.log(data); // undefined
  });
 
// Correct: return the promise
fetch("/users/1")
  .then((res) => res.json())
  .then((data) => console.log(data));

Nesting .then() instead of chaining. If you put a then callback inside another then callback, you lose the flat structure that makes chains easy to read top to bottom.

javascriptjavascript
// Mistake: nested callbacks
fetchUser(1).then((user) => {
  fetchPosts(user.id).then((posts) => {
    console.log(posts);
  });
});
 
// Correct: chain
fetchUser(1)
  .then((user) => fetchPosts(user.id))
  .then((posts) => console.log(posts));

Not handling rejections. An unhandled promise rejection is a bug. Always add a .catch() or use try...catch with await.

Assuming .then() runs synchronously. Even if a promise is already resolved, then callbacks run asynchronously.

javascriptjavascript
Promise.resolve("value").then(console.log);
console.log("This runs first");
// Output: "This runs first", then "value"

Creating a promise that never settles. If you forget to call resolve or reject inside the executor, the promise stays pending forever and any then callback on it never runs.

When to create promises vs consume them

Create a promise whenConsume a promise when
Wrapping a callback-based APIUsing fetch, setTimeout wrappers, or any promise-returning function
Building a delay/sleep utilityChaining then and catch calls
Implementing a custom async operationPassing promises to Promise.all, Promise.race, etc.

Most of the time you consume promises. You only create them when you need to wrap something that does not already return a promise.

For promise chaining and how then values flow through chains, see JavaScript Promise Chaining: A Complete Guide. For the async/await syntax that builds on promises, see JavaScript Async Await Guide: Complete Tutorial.

Rune AI

Rune AI

Key Insights

  • A promise is an object representing a future value. It is pending until it settles.
  • A promise settles once: either fulfilled with a value or rejected with an error.
  • .then() registers a callback for when the promise fulfills.
  • .catch() registers a callback for when the promise rejects.
  • Promises are the foundation of async/await, fetch, and all modern async JavaScript.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between a promise and a callback?

A callback is a function passed to another function to run later. A promise is an object that represents a future value. Promises avoid callback nesting and provide .catch() for centralized error handling instead of error-first callbacks.

Can a promise change state after it is settled?

No. Once a promise resolves or rejects, its state is final. Calling resolve() or reject() again has no effect. A settled promise stays settled forever.

Do I need to create promises, or can I just use them?

Most of the time you use promises returned by APIs like fetch, not create them yourself. You only create promises when wrapping callback-based APIs or building custom async utilities.

Conclusion

A promise is a placeholder for a value that is not available yet. It has three states (pending, fulfilled, rejected), settles only once, and lets you chain reactions with .then() and .catch(). Understanding promises is the gateway to working with async JavaScript.