AlgoInsight

Free lesson · JavaScript Basic to Advance

Promises, From Scratch

Easy5 min read

What Is a Promise?

A Promise is an object representing a future value — a placeholder for something that hasn't happened yet, but eventually will. Either the operation succeeds and the Promise resolves with a value, or it fails and the Promise rejects with an error. Either way, the Promise eventually settles, and code waiting on it can react.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Data loaded");
  }, 100);
});

promise.then(value => {
  console.log("Got:", value);
});

console.log("Promise created, not yet settled");

The output: Promise created, not yet settled first, then 100ms later Got: Data loaded. The Promise is an object you can hold onto and attach handlers to with .then. When the underlying operation completes, the handlers run.

Promises replaced callbacks as the primary async pattern in JavaScript because they solve the problems Topic 9.3 covered — callback hell, repeated error handling, inversion of control. Modern JavaScript code uses Promises everywhere, either directly with .then or through async/await (Topic 9.6). This Topic covers the foundations.

The Three States of a Promise

A Promise is always in one of three states:

Once a Promise is fulfilled or rejected, it's settled and its state never changes again. A Promise can only go from pending to fulfilled, or from pending to rejected. It cannot un-settle, switch to a different state, or settle twice.

graph TD
    A[Pending] --> B[Fulfilled<br/>with a value]
    A --> C[Rejected<br/>with a reason]

    B --> D[Settled — cannot change]
    C --> D

    style A fill:#f59e0b,color:#fff
    style B fill:#10b981,color:#fff
    style C fill:#dc2626,color:#fff
    style D fill:#7c3aed,color:#fff
This diagram shows the three states of a Promise. A Promise starts in the pending state. From there, it can transition to either fulfilled with a value or rejected with a reason. Once it reaches either of those states, it's settled and cannot change again. A Promise cannot un-settle, switch to a different state, or settle a second time. This single-transition property is what makes Promises predictable.

This single-transition property is critical. It's what makes Promises trustworthy in a way callbacks aren't — you can't accidentally trigger a handler twice with a Promise, because the Promise itself enforces the rule.

Interactive animation: Each callback betrayal retired - a promise settles once, ignores extra calls, and lets a rejection flow until handled.

Creating a Promise

The Promise constructor takes a function (called the executor) with two parameters: resolve and reject. The executor runs immediately. Inside it, you do whatever async work you need, then call resolve(value) to fulfill the Promise or reject(reason) to reject it.

function loadData(shouldFail) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error("Loading failed"));
      } else {
        resolve({ name: "Olena", age: 35 });
      }
    }, 50);
  });
}

loadData(false).then(data => console.log("Success:", data));
loadData(true).catch(err => console.log("Error:", err.message));

loadData returns a Promise. The Promise represents the future result of the operation. Callers use .then to handle success and .catch to handle failure.

In practice, you rarely create Promises with new Promise(...) directly. Most modern APIs already return Promises — fetch, fs.promises.readFile, database libraries, etc. You only construct Promises manually when wrapping a callback-style API or building a new async primitive.

Consuming a Promise: then, catch, finally

Interactive animation: Registering with .then never waits - and a handler attached long after the promise settled still receives the value.

A Promise's three main methods:

.then(onFulfilled, onRejected) — attach handlers for success and (optionally) failure.

.catch(onRejected) — shorthand for .then(undefined, onRejected). Handle failure only.

.finally(onFinally) — run a function regardless of success or failure. Doesn't receive the value or error.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("data"), 50);
  });
}

fetchData()
  .then(value => {
    console.log("Got:", value);
    return value.toUpperCase();
  })
  .then(upperValue => {
    console.log("Upper:", upperValue);
  })
  .catch(err => {
    console.log("Error:", err);
  })
  .finally(() => {
    console.log("Done — cleanup runs either way");
  });

.then returns a new Promise. The value the handler returns becomes the value of that new Promise. This is what makes chaining work — you can sequence operations by returning values from each .then.

.catch also returns a new Promise. If you handle an error, the chain can continue with a fallback value. If you don't handle it, the rejection propagates down the chain until something does (or it becomes an unhandled rejection).

.finally is for cleanup — closing connections, hiding loading indicators, logging completion. The function runs whether the Promise was fulfilled or rejected. It doesn't change the value, so subsequent .then calls still see the original value.

Chaining: The Power of Promises

The killer feature of Promises is chaining. Each .then returns a new Promise, which means you can string multiple async operations together without nesting:

function step1() {
  return new Promise(resolve => {
    setTimeout(() => resolve("result1"), 50);
  });
}

function step2(input) {
  return new Promise(resolve => {
    setTimeout(() => resolve(input + " → result2"), 50);
  });
}

function step3(input) {
  return new Promise(resolve => {
    setTimeout(() => resolve(input + " → result3"), 50);
  });
}

// Promise chain — flat, sequential
step1()
  .then(r1 => step2(r1))
  .then(r2 => step3(r2))
  .then(r3 => console.log("Final:", r3))
  .catch(err => console.log("Error:", err));

Compare this with the callback-hell version from Topic 9.3. Three operations, no nesting. Each handler returns the Promise for the next operation. The .then chain reads sequentially, top to bottom.

The key insight: if a .then handler returns a Promise, the next .then waits for that Promise to settle. This is what makes chaining work without nesting. JavaScript automatically flattens the Promise chain.

Interactive animation: The same four steps as a callback pyramid and as a flat promise chain - one column instead of growing depth.

The Promise from Synchronous Values

You can create an already-settled Promise immediately:

const fulfilled = Promise.resolve("value");
const rejected = Promise.reject(new Error("reason"));

fulfilled.then(v => console.log("Fulfilled:", v));
rejected.catch(e => console.log("Rejected:", e.message));

Promise.resolve(value) returns a Promise that's already fulfilled with the value. Promise.reject(reason) returns one that's already rejected.

These are useful in two scenarios:

Wrapping a sync value in async form. Sometimes a function returns either a sync value or a Promise. Wrapping the sync value in Promise.resolve makes everything consistent — the caller can always use .then.

function getCachedOrFetch(key, cache) {
  if (cache[key]) {
    return Promise.resolve(cache[key]);   // sync, but returned as a Promise
  }
  return fetch(`/api/data/${key}`);          // async Promise
}

// Caller can treat both cases uniformly
getCachedOrFetch("user1", { user1: { name: "Olena" } })
  .then(data => console.log(data));

Starting a Promise chain. Promise.resolve().then(...) is a way to schedule a microtask. We saw this in Topic 9.2.

Promise Microtasks

When a Promise settles, its .then callbacks are added to the microtask queue — not the task queue. Topic 9.2 covered this in detail. This is why Promise resolution happens before setTimeout callbacks of the same priority:

console.log("1");

Promise.resolve("microtask").then(value => console.log(value));

setTimeout(() => console.log("task"), 0);

console.log("2");

Output: 1, 2, microtask, task. The Promise's .then callback runs before the setTimeout callback because microtasks have priority. Internalize this — it's one of the most-asked event-loop interview questions.

Common Pitfalls

Warning: The Promise basics traps.

Forgetting to return inside .then. If a .then handler doesn't return a value, the next .then gets undefined. If it doesn't return the Promise from a nested async call, the chain doesn't wait properly. Always think about what each handler returns.

Wrapping non-Promises in new Promise(...). Don't write return new Promise(resolve => resolve(someValue)) — just return Promise.resolve(someValue). Or, if the value is the result of another Promise, return that Promise directly.

Unhandled rejections. A Promise that rejects without anyone calling .catch produces an "unhandled rejection" warning. In modern Node and browsers, unhandled rejections can crash the program. Always either handle errors or let them propagate to a catch handler higher up.

Interactive animation: What each handler returns decides what the next .then receives - a forgotten return passes undefined down the chain.

Why This Matters in Interviews

Interview: "What is a Promise? What problem does it solve?" is a foundational JavaScript interview question. The expected answer covers the three states, the single-transition rule, chaining, and the comparison to callback hell.

Implementation questions also come up: "implement Promise.resolve from scratch" or "explain what Promise.resolve(somePromise) does" (the answer is interesting — it adopts the other Promise's state, doesn't wrap it).

Topic 9.12 — Interview Lens: Promise and async/await Patterns — covers the deeper interview treatment.

Try It

Exercise: Convert this callback-style code to a Promise chain:

function getUser(id, callback) {
  setTimeout(() => callback(null, { id, name: "Olena" }), 50);
}

function getOrders(userId, callback) {
  setTimeout(() => callback(null, [{ id: 1 }, { id: 2 }]), 50);
}

function getOrderDetails(orderId, callback) {
  setTimeout(() => callback(null, { id: orderId, total: 99 }), 50);
}

Wrap each function in a Promise-returning version, then chain them: get a user, get their orders, get the details of the first order. The Promise version should be visibly flatter than the callback version. Once you've done it, you've made the leap from callbacks to Promises.

Back to JavaScript Basic to Advance