JavaScript Promises: The Basics You Need to Know

V
5 min readSep 30, 2024

Introduction

JavaScript is a single-threaded programming language, meaning it can only run one task at a time. This becomes tricky with asynchronous operations like fetching data or setting timers, which can block the flow of execution and slow down your app.

To handle these async tasks without freezing the thread, we encounter Promise — a powerful tool that simplifies asynchronous programming. With Promises, you can manage long-running tasks more effectively, write cleaner, more readable code, and avoid the dreaded “callback hell.

In this article, I aim to familiarize you with what Promises are, how they work, and how they simplify asynchronous programming.

What is a Promise?

Imagine you’re ordering a meal at a restaurant. Once you’ve placed your order, you don’t wait by the kitchen for your food to be prepared. Instead, you go about your conversation or enjoy the ambiance while the kitchen prepares your meal in the background. The restaurant promises to serve you the food once it’s ready. You can trust this promise because, eventually, one of two things will happen: either your meal will arrive (fulfilled), or the kitchen will inform you that they can’t complete the order (rejected).

In JavaScript, Promises work in a similar way. When you ask JavaScript to do something that takes time — like fetching data from a server — it returns a Promise. This Promise doesn’t immediately give you the result. Instead, it tells you, “I’ll get back to you when the work is done.” During that time, the rest of your code continues to run. Once the task is complete, the Promise is either:

  • Fulfilled (the task succeeded), or
  • Rejected (the task failed), and you can handle the outcome accordingly.

How Promises work in JavaScript

A Promise represents a value that may be available now, in the future, or never. It has three states:

  • Pending: The task is still in progress, and the final outcome (fulfilled or rejected) is not yet determined.
  • Fulfilled: The task was completed successfully, and the result is available.
  • Rejected: The task failed, and an error is available to handle

1. Creating a Promise

To create a Promise, you use the Promise constructor, which takes a function (known as the executor) that has two parameters: resolve and reject. The resolve function is called when the Promise is fulfilled, while the reject function is called when it is rejected.

To create a Promise, you use the Promise constructor, which takes a function (known as the executor) that has two parameters: resolve and reject. The resolve function is called when the Promise is fulfilled, while the reject function is called when it is rejected.

const myPromise = new Promise((resolve, reject) => {
// Simulating an asynchronous task (e.g., fetching data)
const success = true; // Simulate success or failure

if (success) {
resolve("Operation completed successfully!"); // Fulfill the promise
} else {
reject("Operation failed."); // Reject the promise
}
});

2. Resolving and Rejecting Promises

Once a Promise is created, you can decide its outcome by calling either resolve or reject:

  • resolve(value): Call this function when the asynchronous operation completes successfully. It passes a value to the handlers that are waiting for the Promise to be fulfilled.
  • reject(error): Call this function when the operation fails. It passes an error message to the handlers that are waiting for the Promise to be rejected.

3. Consuming Promises

Once you’ve created a Promise, the next step is to consume it. JavaScript provides several methods for handling the outcomes of Promises: .then(), .catch(), and .finally(). Each of these methods serves a specific purpose and allows you to effectively manage the results of asynchronous operations.

  • Handling Resolved Promises with .then(): The .then() method is used to specify what should happen when a Promise is fulfilled. It takes two optional arguments: a callback for the resolved value and another for handling rejections.
const fetchData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Data fetched successfully!");
}, 1000);
});
};

fetchData()
.then(result => {
console.log(result); // Logs: Data fetched successfully!
});
  • Handling Rejections with .catch(): The .catch() method is specifically designed to handle errors that occur during the execution of the Promise. This makes it a clean way to deal with rejections.
const fetchWithError = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("Error fetching data."); // Simulating an error
}, 1000);
});
};

fetchWithError()
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error); // Logs: Error fetching data.
});
  • Final or cleanup actions using .finally(): The .finally() method allows you to execute code after the Promise has settled, regardless of whether it was fulfilled or rejected. This is useful for cleanup actions or tasks that should run in both success and failure scenarios.
fetchData()
.then(result => {
console.log(result); // Logs: Data fetched successfully!
})
.catch(error => {
console.error(error); // Handle error
})
.finally(() => {
console.log("Promise has settled."); // Logs after either success or failure
});

To be concise:

  • then(): Use this method to handle the resolved value of a Promise.
  • catch(): Use this method to handle errors when a Promise is rejected.
  • finally(): This method runs code after the Promise settles, regardless of the outcome, allowing for cleanup or final actions.

4. Promise Chaining

One of the most powerful features of Promises is their ability to be chained together, allowing you to perform multiple asynchronous operations in sequence. This means each operation waits for the previous one to complete before executing, which is particularly useful when tasks depend on each other.

Let’s take a look at the following example:

const fetchUserData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ userId: 1, username: "JohnDoe" });
}, 1000);
});
};

const fetchPosts = (userId) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(["Post 1", "Post 2", "Post 3"]); // Simulated posts
}, 1000);
});
};

// Chaining Promises
fetchUserData()
.then(user => {
console.log("User fetched:", user);
return fetchPosts(user.userId); // Pass userId to the next promise
})
.then(posts => {
console.log("Posts fetched:", posts);
})
.catch(error => {
console.error("Error:", error);
});

In this example, the fetchUserData function returns a Promise that resolves with user information. The resolved value is then passed to the fetchPosts function, which returns another Promise. If any of these Promises are rejected, the error is caught in the final .catch() method, allowing for effective error handling throughout the chain.

Conclusion

In conclusion, Promises are a crucial part of modern JavaScript, enabling developers to handle asynchronous operations in a more structured and efficient way. By using Promises, you can:

  • Simplify the management of asynchronous tasks and avoid callback hell.
  • Chain multiple asynchronous operations to maintain a clear flow of execution.
  • Effectively handle errors with a unified approach.

As you implement Promises in your own projects, you’ll find that they not only improve code readability but also enhance the overall user experience by keeping your applications responsive. I hope that this journey through JavaScript’s foundational concepts has provided valuable insights for developers. Happy coding!

--

--

No responses yet