DEV Community

Rahul Vijayvergiya
Rahul Vijayvergiya

Posted on • Originally published at rahulvijayvergiya.hashnode.dev

Understanding JavaScript Promises

This post was initially published on my blog. Check out the original source using the link below:

Understanding JavaScript Promises

Learn the essentials of JavaScript promises, including their creation, usage, and common pitfalls. Understand how to handle asynchronous operations.

favicon rahulvijayvergiya.hashnode.dev

JavaScript promises are a powerful way to handle asynchronous operations. Before the introduction of promises, callbacks were the primary method to handle asynchronous tasks. However, callbacks can lead to "callback hell," where nested callbacks become difficult to manage and read. Promises provide a cleaner, more readable way to manage asynchronous operations.

What is a Promise?

A promise in JavaScript represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It is an object that can be in one of three states:

  • Pending: Initial state, neither fulfilled nor rejected.
  • Fulfilled: Operation completed successfully.
  • Rejected: Operation failed.

Creating a Promise

To create a promise, use the Promise constructor, which takes a function with two parameters: resolve and reject. These functions control the promise's state.

const myPromise = new Promise((resolve, reject) => {
  let success = true; // Simulate an operation
  if (success) {
    resolve("Operation successful!");
  } else {
    reject("Operation failed.");
  }
});
Enter fullscreen mode Exit fullscreen mode

Using Promises

Promises have then, catch, and finally methods to handle the result.

  • then: Called when the promise is fulfilled.
  • catch: Called when the promise is rejected.
  • finally: Called regardless of the promise's outcome.
myPromise
  .then((message) => {
    console.log(message); // "Operation successful!"
  })
  .catch((error) => {
    console.error(error); // "Operation failed."
  })
  .finally(() => {
    console.log("Operation completed.");
  });
Enter fullscreen mode Exit fullscreen mode

Chaining Promises

One of the main advantages of promises is chaining. You can chain multiple then calls to perform a sequence of asynchronous operations.

const firstPromise = new Promise((resolve) => {
  setTimeout(() => resolve("First promise resolved"), 1000);
});

firstPromise
  .then((result) => {
    console.log(result); // "First promise resolved"
    return new Promise((resolve) => {
      setTimeout(() => resolve("Second promise resolved"), 1000);
    });
  })
  .then((result) => {
    console.log(result); // "Second promise resolved"
  })
  .catch((error) => {
    console.error(error);
  });
Enter fullscreen mode Exit fullscreen mode

Handling Multiple Promises

JavaScript provides utility methods to handle multiple promises concurrently: Promise.all, Promise.race, Promise.allSettled, and Promise.any.

  • Promise.all: Waits for all promises to resolve or any to reject.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, 'foo'));

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // [3, 42, 'foo']
});
Enter fullscreen mode Exit fullscreen mode
  • Promise.race: Resolves or rejects as soon as one of the promises resolves or rejects.
const promise1 = new Promise((resolve) => setTimeout(resolve, 500, 'one'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'two'));

Promise.race([promise1, promise2]).then((value) => {
  console.log(value); // "two"
});
Enter fullscreen mode Exit fullscreen mode
  • Promise.allSettled: Waits for all promises to settle (either resolve or reject).
const promise1 = Promise.resolve(42);
const promise2 = Promise.reject('error');
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, 'foo'));

Promise.allSettled([promise1, promise2, promise3]).then((results) => {
  results.forEach((result) => console.log(result.status));
  // "fulfilled"
  // "rejected"
  // "fulfilled"
});
Enter fullscreen mode Exit fullscreen mode
  • Promise.any: Waits for any promise to resolve.
const promise1 = Promise.reject('error1');
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'foo'));

Promise.any([promise1, promise2]).then((value) => {
  console.log(value); // "foo"
}).catch((error) => {
  console.error(error); // AggregateError: All promises were rejected
});
Enter fullscreen mode Exit fullscreen mode

Common Mistakes with Promises

While promises are powerful, they can also be a source of confusion and errors. Here are some common mistakes developers make when working with promises:

1. Not Returning a Promise in a then Handler

For chaining to work correctly, you must return a promise inside a then handler. Failing to do so breaks the chain.

firstPromise
  .then((result) => {
    console.log(result);
    new Promise((resolve) => setTimeout(resolve, 1000, 'Second promise resolved')); // Missing return
  })
  .then((result) => {
    console.log(result); // This will log undefined
  });
Enter fullscreen mode Exit fullscreen mode

2. Catching Errors Too Early

Catching an error early in a chain can prevent downstream then handlers from being aware of the error.

firstPromise
  .then((result) => {
    throw new Error('Something went wrong');
  })
  .catch((error) => {
    console.error(error); // Caught too early
  })
  .then(() => {
    console.log('This will still run');
  });
Enter fullscreen mode Exit fullscreen mode

To avoid this, place error handling at the end of the chain or as close to the specific operation as possible.

3. Creating Unnecessary Promises

It's common to wrap synchronous operations in promises unnecessarily, which adds complexity.

const promise = new Promise((resolve) => {
  resolve('Hello, World!');
});
promise.then((message) => console.log(message));
Enter fullscreen mode Exit fullscreen mode

Instead, just use the value directly:

Promise.resolve('Hello, World!').then((message) => console.log(message));
Enter fullscreen mode Exit fullscreen mode

4. Forgetting to Handle Rejections

Always handle rejections to prevent unhandled promise rejections, which can cause issues in your application.

const promise = new Promise((_, reject) => {
  reject('Error occurred');
});
promise.then((message) => console.log(message)); // No catch handler
Enter fullscreen mode Exit fullscreen mode

Always add a catch block:

promise
  .then((message) => console.log(message))
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

5. Mixing Promises and Async/Await

While promises and async/await are both ways to handle asynchronous code, mixing them can lead to confusion.

async function fetchData() {
  const data = await fetch('https://api.example.com/data');
  data.then((result) => console.log(result)); // Unnecessary promise
}
Enter fullscreen mode Exit fullscreen mode

Stick to one approach:

async function fetchData() {
  const data = await fetch('https://api.example.com/data');
  console.log(await data.json());
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Promises are an essential part of modern JavaScript, providing a robust way to handle asynchronous operations. By understanding how to create, use, and manage promises, you can write cleaner, more maintainable code. Avoiding common mistakes will help you leverage promises effectively and prevent potential pitfalls. Whether you're handling a single asynchronous task or coordinating multiple concurrent operations, promises are a powerful tool in your JavaScript toolkit.

This post was initially published on my blog. Check out the original source using the link below:

Understanding JavaScript Promises

Learn the essentials of JavaScript promises, including their creation, usage, and common pitfalls. Understand how to handle asynchronous operations.

favicon rahulvijayvergiya.hashnode.dev

Top comments (1)

Collapse
 
oculus42 profile image
Samuel Rouse

Hello! I'm a huge fan of Promises and always excited to see more articles about them!

I noticed some issues in your article that could be confusing. Under Common Mistakes, you wrote the following:

For chaining to work correctly, you must return a promise inside a then handler. Failing to do so breaks the chain.

I think this may just be a wording issue, but worry that people may misunderstand the function of promise chains.

In your example you do need to return the nested promise created inside that function, but it is more about the missing return value, not that it needs to be a promise. The issue you are experiencing is not returning any value passes undefined to the next step. This isn't really "breaking" the promise chain, but it can be confusing or create problems when we expect a value.

Once you are in a promise chain any returned value will be automatically handled as a promise. If you return a promise it will be used, but a synchronous value is perfectly fine. Synchronous functions work inside promise chains without any issues.

As an example, here's a very simple promise chain using a synchronous function and return value.

// Synchronous function
const double = value => value * 2;

Promise.resolve(2)
.then(double)
.then(console.log); // 4
Enter fullscreen mode Exit fullscreen mode

I hope that helps clear up any misunderstanding.