Skip to content

Promises

This lesson explains Promises with clear examples, use cases, and best practices so you can use modern JavaScript confidently in real projects.

ES6+ Promises Overview

A Promise is an object that represents the eventual result of an asynchronous operation. It helps you manage async code without deeply nested callbacks and gives you a clear way to handle success and failure.

Promises are the foundation of modern async JavaScript. They power the Fetch API, database calls, file uploads, timers, and async/await syntax.

Feature Description
Introduced In ES6 (ECMAScript 2015)
States Pending, fulfilled, rejected
Main Methods .then(), .catch(), .finally()
Creation new Promise((resolve, reject) => ...)
Static Helpers Promise.resolve(), Promise.reject()
Common Uses API requests, timers, file I/O, async workflows

Basic Promise Example

const promise =
  new Promise((resolve, reject) => {
    const success = true;

    if (success) {
      resolve("Data loaded");
    } else {
      reject(
        new Error("Failed to load")
      );
    }
  });

promise
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error.message);
  });

A Promise starts in the pending state. It becomes fulfilled when resolve() runs or rejected when reject() runs.

How Promises Work

Promises let you attach handlers for success and failure instead of passing callbacks deeper and deeper into nested functions.

Step Description Example
Create Promise Start async work inside the executor. new Promise(...)
Pending State Operation is still running. Waiting for API response
Resolve Operation succeeds with a value. resolve(data)
Reject Operation fails with an error. reject(error)
Handle Result Use then, catch, or finally. .then(...)

Promise States

State Meaning What Happens Next
Pending Async work is still in progress. Can move to fulfilled or rejected.
Fulfilled Operation completed successfully. .then() handlers run.
Rejected Operation failed. .catch() handlers run.

then, catch, and finally

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({
          id,
          name: "Alex"
        });
      } else {
        reject(
          new Error("Invalid user id")
        );
      }
    }, 1000);
  });
}

fetchUser(1)
  .then((user) => {
    console.log(user.name);
    return user.id;
  })
  .then((id) => {
    console.log("User ID:", id);
  })
  .catch((error) => {
    console.error(error.message);
  })
  .finally(() => {
    console.log("Request finished");
  });

.then() handles success, .catch() handles errors, and .finally() runs whether the Promise succeeds or fails.

Promise Chaining

function getToken() {
  return Promise.resolve("abc123");
}

function getProfile(token) {
  return Promise.resolve({
    token,
    name: "Sam"
  });
}

function getSettings(profile) {
  return Promise.resolve({
    ...profile,
    theme: "dark"
  });
}

getToken()
  .then(getProfile)
  .then(getSettings)
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error);
  });

Each .then() can return a value or another Promise. This creates a readable async pipeline instead of callback nesting.

Fetch API with Promises

fetch(
  "https://jsonplaceholder.typicode.com/users/1"
)
  .then((response) => {
    if (!response.ok) {
      throw new Error(
        "Network response failed"
      );
    }

    return response.json();
  })
  .then((user) => {
    console.log(user.name);
    console.log(user.email);
  })
  .catch((error) => {
    console.error(error.message);
  });

The Fetch API returns a Promise, making it one of the most common real-world uses of Promises in frontend development.

Promise.resolve and Promise.reject

const success =
  Promise.resolve("Ready");

const failure =
  Promise.reject(
    new Error("Something went wrong")
  );

success.then((value) => {
  console.log(value);
});

failure.catch((error) => {
  console.error(error.message);
});

These static methods quickly create already-settled Promises. They are useful for wrapping values or starting error flows in tests and utilities.

Promise Error Handling

function divide(a, b) {
  return new Promise((resolve, reject) => {
    if (b === 0) {
      reject(
        new Error("Cannot divide by zero")
      );
      return;
    }

    resolve(a / b);
  });
}

divide(10, 0)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error.message);
  });

divide(10, 2)
  .then((result) => {
    console.log(result);
  });

Always handle rejected Promises with .catch() or by returning the Promise to a caller that handles errors. Unhandled rejections can cause hard-to-debug runtime issues.

Promise with setTimeout

function wait(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

wait(1500)
  .then(() => {
    console.log("Done waiting");
  });

Wrapping timers in Promises is a common pattern for delays, retries, polling, and staged async workflows.

Callbacks vs Promises

// Callback style

function loadUserCallback(id, callback) {
  setTimeout(() => {
    callback(null, { id, name: "Alex" });
  }, 500);
}

// Promise style

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

loadUserPromise(1)
  .then((user) => {
    console.log(user);
  });

Promises reduce callback nesting and make async flows easier to compose, chain, and debug compared to traditional callback patterns.

Promises vs async/await

Feature Promises async/await
Syntax .then() and .catch() await inside async functions
Readability Good for pipelines and chaining. Looks closer to synchronous code.
Foundation Core async primitive. Built on top of Promises.
Error Handling .catch() try/catch

Common Promise Use Cases

  • Fetching data from APIs with Fetch or Axios.
  • Reading files or database records asynchronously.
  • Running delayed or scheduled tasks with timers.
  • Chaining multiple async steps in order.
  • Wrapping callback-based APIs in Promise-based helpers.
  • Handling success and failure in UI loading states.
  • Building the base layer for async/await functions.

Promise Best Practices

  • Always handle errors with .catch() or upstream handlers.
  • Return Promises from .then() when chaining async work.
  • Prefer async/await for long sequential flows when readability matters.
  • Reject with meaningful Error objects, not raw strings.
  • Use .finally() for cleanup such as hiding loaders.
  • Avoid creating unnecessary Promise wrappers around sync values.
  • Keep Promise chains flat and focused on one workflow.

Common Promise Mistakes

  • Forgetting to return a Promise inside .then().
  • Not handling rejected Promises at all.
  • Creating callback hell inside Promise executors.
  • Calling resolve() and reject() more than once.
  • Mixing callbacks and Promises in the same function without a clear pattern.
  • Assuming .then() runs synchronously.
  • Throwing errors outside a .catch() or rejected flow.

Key Takeaways

  • Promises represent the future result of async work.
  • They have three states: pending, fulfilled, and rejected.
  • Use .then(), .catch(), and .finally() to handle outcomes.
  • Promise chaining makes async workflows easier to read and maintain.
  • Promises are the foundation for Fetch, timers, and async/await.

Pro Tip

When chaining Promises, always return the next Promise from .then(). Without a return, the next step receives undefined instead of the async result.