DEV Community

Kalyan P C
Kalyan P C

Posted on

Understanding Promises in JavaScript

Promises are a fundamental part of modern JavaScript, introduced in ECMAScript 2015 (ES6). They provide a way to handle asynchronous operations more cleanly and predictably than traditional callbacks.

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It's like a "placeholder" for a value that might not be available yet.

  • Promises were created to solve callback hell (deeply nested callbacks in async code).
  • They allow you to write asynchronous code that looks more like synchronous code (especially with async/await, but we'll focus on raw promises here).
  • Promises are immutable once settled — their value or reason can't change after fulfillment or rejection.

Promise States

Every Promise goes through one of the three states(it starts in "pending" and moves to one of the other two):

  1. Pending: Initial state — the operation is ongoing, neither fulfilled nor rejected.
  2. Fulfilled(Resolved): The operation completed successfully, and the Promise has a value.
  3. Rejected: The operation failed, and the Promise has a reason (usually an Error object)

Creating a Promise

you create a Promise using the Promise constructor, which takes and executor funtion:

const myPromise = new Promise((resolve, reject) => {
  // Async operation here...
  if (success) {
    resolve(value);  // Fulfill with value
  } else {
    reject(reason);  // Reject with reason (e.g., new Error("Failed"))
  }
}
Enter fullscreen mode Exit fullscreen mode
  • resolve(value): Settles to fulfilled.
  • reject(reason): Settles to rejected.
  • the executor runs immediately(synchronously) when the Promise is created.

Handling Promise Outcomes

You attach handlers using methods (these return new Promises for chaining):

  • .then(onFulfilled, onRejected): Handles fulfillment (first arg) or rejection (second arg).

    • If fulfilled → calls onFulfilled(value)
    • If rejected → calls onRejected(reason)
  • .catch(onRejected): Shortcut for .then(null, onRejected) — only handles rejection. Always use .catch — uncaught rejections are bad.

  • .finally(onFinally): Runs regardless of outcome (no access to value/reason) — useful for cleanup.

Chaining and Error Propagation

  • Each .then/.catch/.finally returns a new Promise, allowing chaining.
  • If a handler returns a value → next .then gets that value.
  • If a handler throws an error or returns a rejected Promise → skips to next .catch.
  • Unhandled rejections trigger a console warning (and in Node.js, can crash the process if not handled).

Here are some examples to understand how it works

Example 1: Basic Promise Creation and Resolution

A simple Promise that resolves after a delay (simulating async like setTimeout).

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Success! Data loaded.");
  }, 1000);  // 1 second delay
});

myPromise
  .then(result => {
    console.log("Fulfilled:", result);  // "Fulfilled: Success! Data loaded."
  })
  .catch(error => {
    console.error("Rejected:", error);
  })
  .finally(() => {
    console.log("Finally: Operation complete.");  // Always runs
  });
Enter fullscreen mode Exit fullscreen mode
  • What happens: Promise starts pending → resolves after 1s → .then runs → .finally runs.

Example 2: Rejection and Error Handling

Simulate a failure.

const failingPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("Failed to load data."));
  }, 1000);
});

failingPromise
  .then(result => console.log("Won't run:", result))
  .catch(error => console.error("Caught error:", error.message))  // "Caught error: Failed to load data."
  .finally(() => console.log("Cleanup done."));
Enter fullscreen mode Exit fullscreen mode
  • Key: .then's first handler skips on rejection → .catch handles it.

Example 3: Chaining Promises

Chain multiple async operations.

function fetchUser(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id, name: "Alice" }), 500);
  });
}

function fetchPosts(user) {
  return new Promise(resolve => {
    setTimeout(() => resolve(["Post 1", "Post 2"]), 500);
  });
}

fetchUser(1)
  .then(user => {
    console.log("User:", user);
    return fetchPosts(user);  // Return a new Promise for chaining
  })
  .then(posts => {
    console.log("Posts:", posts);
  })
  .catch(error => console.error("Error in chain:", error));
Enter fullscreen mode Exit fullscreen mode
  • Output: Logs user after 500ms → posts after another 500ms.
  • Propagation: If fetchUser rejects → skips to .catch.

Example 4: Error in Chain

Throw inside a handler → propagates.

Promise.resolve(10)
  .then(value => {
    console.log("First:", value);  // 10
    throw new Error("Oops!");     // Throws → rejects next Promise
  })
  .then(value => console.log("Won't run:", value))
  .catch(error => console.error("Caught:", error.message));  // "Caught: Oops!"
Enter fullscreen mode Exit fullscreen mode
Code Playground

Key Concepts

Promise handlers (.then/.catch) run in the microtask queue — higher priority than macrotasks (e.g., setTimeout). They execute after current sync code but before next event loop tick.

async/await (ES2017) builds on Promises, making them look synchronous (e.g., const value = await myPromise;).

Thanks for reading. Happy coding!!!

Top comments (0)