Skip to main content

Command Palette

Search for a command to run...

JavaScript Promises Explained for Beginners

Updated
6 min readView as Markdown

JavaScript is single-threaded, meaning it executes one task at a time. But modern applications constantly deal with operations that take time, such as:

  • Fetching data from an API

  • Reading files

  • Accessing databases

  • Waiting for timers

  • Uploading images

If JavaScript waited for each operation to finish before moving forward, applications would become slow and unresponsive.

This is where Promises become important.

Promises provide a cleaner and more organized way to handle asynchronous operations in JavaScript.


The Problem Promises Solve

Before promises existed, developers mainly used callbacks to handle asynchronous code.

Example Using Callbacks

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

console.log("Loading...");

Output

Loading...
Data received

The timer runs in the background while JavaScript continues executing other code.

Callbacks work fine for simple tasks. But when multiple asynchronous operations depend on each other, the code becomes messy.


Callback Hell Problem

loginUser(function(user) {

  getUserPosts(user, function(posts) {

    getPostComments(posts[0], function(comments) {

      console.log(comments);

    });

  });

});

This deeply nested structure becomes:

  • Hard to read

  • Difficult to debug

  • Difficult to maintain

  • Error-prone

This problem is commonly called Callback Hell.

Promises were introduced to solve this readability and structure problem.


What is a Promise?

A Promise is an object that represents a value that may become available in the future.

Think of a promise like ordering food online.

  • You place the order.

  • The food is being prepared.

  • Eventually:

    • The order arrives successfully

    • OR the order gets cancelled

Similarly, a JavaScript promise represents an operation that is still in progress and will eventually either succeed or fail.


Promise States

A promise has three possible states.

1. Pending

The operation is still running.

Pending...

The result is not available yet.


2. Fulfilled

The operation completed successfully.

Promise fulfilled

A successful value is returned.


3. Rejected

The operation failed.

Promise rejected

An error or failure reason is returned.


Basic Promise Syntax

Creating a Promise

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

  let success = true;

  if (success) {
    resolve("Data fetched successfully");
  } else {
    reject("Something went wrong");
  }

});

Understanding resolve and reject

Inside a promise:

  • resolve() means success

  • reject() means failure

Only one of them will run.


Basic Promise Lifecycle

Here is the normal lifecycle of a promise:

  1. Promise starts in pending

  2. Operation runs

  3. Promise becomes:

    • fulfilled

    • OR rejected


Handling Success and Failure

Promises use:

  • .then() for success

  • .catch() for failure

Example

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

  let success = true;

  if (success) {
    resolve("Login successful");
  } else {
    reject("Login failed");
  }

});

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

Output When Successful

Login successful

Output When Failed

Login failed

Why Promises Improve Readability

Compare these two approaches.

Callback Version

getData(function(result) {

  processData(result, function(finalData) {

    saveData(finalData, function(response) {

      console.log(response);

    });

  });

});

Promise Version

getData()
  .then(processData)
  .then(saveData)
  .then((response) => {
    console.log(response);
  })
  .catch((error) => {
    console.log(error);
  });

The promise version is:

  • Cleaner

  • Easier to follow

  • Less nested

  • Easier to maintain

This is one of the biggest reasons promises became popular.


Promise Chaining Concept

Promise chaining means connecting multiple asynchronous operations together.

Each .then() returns another promise.

This allows sequential execution.


Example of Promise Chaining

function step1() {
  return new Promise((resolve) => {
    resolve("Step 1 completed");
  });
}

function step2(previousResult) {
  return new Promise((resolve) => {
    resolve(previousResult + " -> Step 2 completed");
  });
}

function step3(previousResult) {
  return new Promise((resolve) => {
    resolve(previousResult + " -> Step 3 completed");
  });
}

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

Output

Step 1 completed -> Step 2 completed -> Step 3 completed

This structure is much easier to understand compared to deeply nested callbacks.


Real-World Example: Fetching API Data

Promises are heavily used in API requests.

fetch("https://api.example.com/users")
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log("Error:", error);
  });

How Error Handling Works

If any promise in the chain fails, control immediately moves to .catch().

doTask1()
  .then(doTask2)
  .then(doTask3)
  .catch((error) => {
    console.log("Something failed:", error);
  });

This centralized error handling is much cleaner than handling errors separately in nested callbacks.


Promises vs Callbacks

Feature Callbacks Promises
Readability Can become messy Cleaner structure
Nesting Deep nesting possible Flat chaining
Error Handling Harder Centralized
Maintainability Difficult in large apps Easier
Sequential Async Tasks Complicated Simple

Important Things to Remember

A Promise Represents Future Data

The result is not immediately available.


A Promise Can Only Settle Once

A promise can either:

  • Fulfill once

  • Reject once

It cannot change state again afterward.


.then() Returns Another Promise

This enables chaining.


Common Beginner Mistakes

Forgetting to Return a Promise

Wrong:

.then(() => {
  fetchData();
})

Correct:

.then(() => {
  return fetchData();
})

Without returning, chaining can break.


Not Handling Errors

Wrong:

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

Correct:

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

Always handle failures properly.


Promises and Async/Await

async/await is built on top of promises.

Promises are still working underneath.

Example:

async function getUsers() {

  try {

    const response = await fetch("https://api.example.com/users");

    const data = await response.json();

    console.log(data);

  } catch(error) {

    console.log(error);

  }

}

Async/await simply makes promise-based code look more synchronous and readable.


Conclusion

Promises changed how JavaScript handles asynchronous operations.

They solve major problems caused by callback-based code by providing:

  • Better readability

  • Cleaner asynchronous flow

  • Easier error handling

  • Better scalability for large applications

Understanding promises is essential because modern JavaScript development heavily depends on them.

1 views