Skip to main content

Command Palette

Search for a command to run...

JavaScript Promises Explained for Beginners

Updated
5 min read

JavaScript is single-threaded, but it often needs to handle tasks that take time—like fetching data from an API, reading a file, or waiting for a timer.

If JavaScript waited for each task to finish before moving on, the whole app would freeze.

That’s where Promises come in.

A Promise is a JavaScript object that represents a value that will be available now, later, or maybe never.

In simple words:

A Promise is a future result of an asynchronous operation.


Why Were Promises Introduced?

Before promises, developers mostly used callbacks for asynchronous code.

Callback Example :-

setTimeout(() => {
  console.log("Data loaded");
}, 2000);

This works, but when many async operations depend on each other, callbacks can become messy.

Callback Hell Example:-

getUser(function(user) {
  getOrders(user.id, function(orders) {
    getPayment(orders[0], function(payment) {
      console.log(payment);
    });
  });
});

This deeply nested structure becomes hard to:

  • read

  • debug

  • maintain

Promises were introduced to make asynchronous code:

  • more readable

  • easier to manage

  • better for chaining multiple async tasks


What Problem Do Promises Solve?

Promises solve the problem of managing asynchronous operations cleanly.

They help with:

  • Avoiding callback hell

  • Handling success and errors in a structured way

  • Chaining async tasks in a readable manner

  • Writing more predictable async code


Stages of a promise:-

A Promise has 3 states:

  1. Pending

    • Initial state

    • Operation is still running

  2. Fulfilled

    • Operation completed successfully

    • Promise returns a value

  3. Rejected

    • Operation failed

    • Promise returns an error/reason

Important:

A promise can only settle once.

That means:

  • pending → fulfilled
    or

  • pending → rejected

Once settled, it cannot change again.

Example:-

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

  if (success) {
    resolve("Task completed successfully");
  } else {
    reject("Task failed");
  }
});

Explanation:

  • new Promise(...) creates a promise

  • It takes a function called the executor

  • The executor receives:

    • resolve → call when task succeeds

    • reject → call when task fails


Handling Promise Results

Promises are usually handled with:

  • .then() → for success

  • .catch() → for failure

  • .finally() → runs no matter what

Example:-

myPromise
    .then((result) => {
    console.log("Success:", result);
  })
  .catch((error) => {
    console.log("Error:", error);
  })
  .finally(() => {
    console.log("Promise finished");
  });

Promise Lifecycle Example:-

const fetchData = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Data received from server");
  }, 2000);
});

console.log("Start");

fetchData
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log(error);
  });

console.log("End");

Output:-

Start
End
Data received from server

Why?

Because:

  • Promise starts async work

  • JavaScript does not block

  • It continues running the next lines

  • When the async task completes, .then() runs later

This is what makes JavaScript non-blocking.


Promise Chaining

One of the biggest benefits of promises is chaining.

Instead of nesting callbacks, you can return values from .then() and continue the flow.

Example:-

const orderFood = new Promise((resolve) => {
  setTimeout(() => {
    resolve("Pizza");
  }, 1000);
});

orderFood
  .then((food) => {
    console.log("Ordered:", food);
    return food + " with extra cheese";
  })
  .then((updatedOrder) => {
    console.log("Updated Order:", updatedOrder);
    return "Bill generated";
  })
  .then((billStatus) => {
    console.log(billStatus);
  })
  .catch((error) => {
    console.log("Something went wrong:", error);
  });

Why chaining is useful:

  • cleaner flow

  • avoids deep nesting

  • easier to debug

  • easier to maintain


Returning a Promise Inside .then()

Sometimes the next step is also asynchronous.

Example:-

function step1() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Step 1 done"), 1000);
  });
}

function step2(message) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(message + " → Step 2 done"), 1000);
  });
}

step1()
  .then((result) => {
    console.log(result);
    return step2(result);
  })
  .then((finalResult) => {
    console.log(finalResult);
  })
  .catch((error) => {
    console.log(error);
  });

If a .then() returns a promise, the next .then() waits for it to finish.

This is a very important promise concept.


Handling Errors in Promises

Errors can happen in two ways:

reject() is called

  • code inside .then() throws an error

Example:-

const riskyTask = new Promise((resolve, reject) => {
  let success = false;

  if (success) {
    resolve("All good");
  } else {
    reject("Something failed");
  }
});

riskyTask
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log("Caught error:", error);
  });

Errors in Chaining:-

Promise.resolve(10)
  .then((num) => {
    return num * 2;
  })
  .then((num) => {
    throw new Error("Unexpected issue");
  })
  .then((num) => {
    console.log(num);
  })
  .catch((error) => {
    console.log("Error handled:", error.message);
  });

If an error happens anywhere in the chain, .catch() can handle it.


Real-World Example: API Request

Promises are commonly used when calling APIs.

fetch("https://jsonplaceholder.typicode.com/users/1")
  .then((response) => response.json())
  .then((user) => {
    console.log("User name:", user.name);
  })
  .catch((error) => {
    console.log("API Error:", error);
  });

Here:

  • fetch() returns a promise

  • response.json() also returns a promise

  • That’s why chaining is needed