
Promises in JavaScript: An Introduction

JavaScript is an asynchronous language, meaning that it allows multiple operations to run concurrently without blocking the execution of other code. This is great for web development, as it allows for responsive and interactive user interfaces. However, it can be challenging to manage asynchronous operations, especially when dealing with multiple asynchronous tasks. That's where promises come in.
Promises are a feature introduced in ES6 to handle asynchronous operations. They provide a way to handle asynchronous operations and their results in a more readable and manageable way. Promises allow you to write cleaner and more concise code by replacing complex nested callbacks with a chain of then() and catch() methods.
Fetching data from an API, loading resources, and executing heavy computations are all examples of asynchronous operations. However, handling asynchronous code can be challenging and lead to callback hell.
Promises work to resolve this by presenting an easy‑to‑understand pattern in JavaScript. In this article, we will introduce promises in JavaScript, how they work, and how they can be used in real‑world scenarios.
What are Promises?
A promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. In other words: a promise is a placeholder for a future value that we expect to receive and can act upon once it arrives.
A promise has three states:
Pending:
The initial state of a promise when it is created.Fulfilled:
The state of a promise when it has been resolved successfully and has value.Rejected:
The state of a promise when it has been rejected with a reason, indicating that the operation failed.
A promise can transition from the pending state to either the fulfilled or rejected state, but once it is in one of these states, it cannot transition to any other state.
Creating a Promise
To create a promise in JavaScript, we use the Promise constructor, which takes a function as its argument. This function is called the executor function and is responsible for initiating the asynchronous operation.
To start with, let's look at an example where we create a promise which resolves after a timeout of 2 seconds:
const myPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve('Promise resolved!'); }, 2000);});In this example, we create a new promise using the Promise constructor and pass an executor function as its argument. The executor function takes two parameters: resolve and reject. These are callback functions that are used to transition the promise to either the fulfilled or rejected state.
The executor function contains the asynchronous operation, which is a setTimeout which resolves the promise after a timeout of 2 seconds. When the promise is resolved, it returns the string 'Promise resolved!'
Using Promises
To consume a promise, we use the then() method, which is called on the promise object. The then() method takes two callback functions as its arguments: one to handle the fulfilled state and one to handle the rejected state.
Here's an example using myPromise from the previous example:
myPromise.then( (result) => { console.log(result); //=> 'Promise resolved!' }, (error) => { console.log(error); // never gets called in this example });In this example, we call the then() method on the myPromise object and pass two callback functions as its arguments. The first callback function is called if the promise is fulfilled (result) and receives the result of the promise, which in this case is the string 'Promise resolved!'.
The second callback function (error) is called if the promise is rejected, but since we didn't reject the promise in our example, this will never get called.
Chaining Promises
Promises can be chained together to handle a sequence of asynchronous operations. When a promise is fulfilled, we can return another promise, which allows us to chain multiple asynchronous operations together.
Let's delve into a more complex, real‑world example:
const getUser = (userId) => { return new Promise((resolve, reject) => { fetch(`/users/${userId}`) .then((response) => { if (response.ok) { resolve(response.json()); } else { reject(new Error('Failed to fetch user')); } }) .catch((error) => reject(error)); });};const getPosts = (userId) => { return new Promise((resolve, reject) => { fetch(`/users/${userId}/posts`) .then((response) => { if (response.ok) { resolve(response.json()); } else { reject(new Error('Failed to fetch posts')); } }) .catch((error) => reject(error)); });};getUser(1) .then((user) => getPosts(user.id)) .then((posts) => { // handle posts data }) .catch((error) => { // handle error });This is a modified example from a project I've recently worked on. Here, we define two functions that return promises: getUser and getPosts. The getUser function fetches a user from an API and returns a promise that resolves with the user data. The getPosts function takes a user ID as a parameter and fetches the posts for that user from the same API. It returns a promise that resolves with that user's post data.
As an aside, the more eagle‑eyed amongst you may have also noticed that I've used template literals in there to create our API endpoint URLs.
We then call these both by chaining the two functions together using the then() method. First, we call getUser() and then we use the then() method to pass the user object to the getPosts() function. This allows us to fetch the posts for the user and handle them in the same chain of promises.
Limitations and Pitfalls
While promises are a powerful pattern for handling asynchronous code, there are some limitations and pitfalls to be aware of.
Forgetting to Handle Errors
One common pitfall I've seen during many code reviews is forgetting to handle errors. We can have the most robust API in the world, and it will still fail from time to time. When using promises, it's important to handle errors by providing a callback function for the rejected state of the promise. If an error occurs and there is no error handler, the error will be silently ignored.
Misunderstanding then()
Another pitfall is the misunderstanding of the then() method. The then() method returns a new promise that is resolved with the value returned by the callback function. If the callback function returns a promise, the new promise will be resolved with the value of the returned promise, not the promise itself. This can lead to unexpected behaviour if not understood properly.
Not a One‑Size‑Fits‑All Remedy
Finally, it's worth noting that promises are not a silver bullet for all asynchronous problems and development. They are just one pattern for dealing with asynchronous code and are not suitable for every situation. It's important to consider other patterns and techniques, such as callbacks or async/await, depending on the use case.
The Wrap‑Up
Promises are a powerful pattern in JavaScript for handling asynchronous operations. They provide a better way to deal with asynchronous code and help avoid callback hell. By understanding how promises work and their limitations, developers can write more robust and maintainable code.
Categories:
Related Articles

Function Declarations vs. Function Expressions vs. Arrow Functions. 
Leveraging .then() in Modern JavaScript. Leveraging
.then()in Modern JavaScript
Simplify Asynchronous JavaScript with async/await. Simplify Asynchronous JavaScript with
async/await
The Difference Between JavaScript Callbacks and Promises. The Difference Between JavaScript Callbacks and Promises

Getting Started with Callbacks in JavaScript. Getting Started with Callbacks in JavaScript

Using Middleware in Next.js for Route Protection. Using Middleware in Next.js for Route Protection

Cleaning up Your JavaScript Code: The Double Bang (!!) Operator. Cleaning up Your JavaScript Code: The Double Bang (
!!) Operator
What are Higher‑Order Components in React? What are Higher‑Order Components in React?

Building Custom Directives in Angular. Building Custom Directives in Angular

Vue 3 Reactivity: Proxies vs. Vue 2 Reactivity. Vue 3 Reactivity: Proxies vs. Vue 2 Reactivity

The Fetch API for Beginners: Get, Post, JSON, and Errors. The Fetch API for Beginners: Get, Post, JSON, and Errors

Creating Custom Vue Directives for Enhanced Functionality. Creating Custom Vue Directives for Enhanced Functionality