A simple introduction to JavaScript promises
Armstrong Olusoji
Posted on May 27, 2024
Introduction
Modern web development requires asynchronicity. Sometimes, a function needs to run only if a prior function is successful. At other times, multiple tasks within an application must occur simultaneously. Functions that need to run simultaneously are asynchronous.
Programming languages, however, are not all equal. Java and the C family are multi-threaded, meaning they can handle asynchronous operations naturally. JavaScript, however, is a single-threaded language. Special functions are therefore needed to manage asynchronous operations in JavaScript. Async / Await and Promises are two such functions. This article will focus on Promises.
This article will cover the following topics:
- Defining JavaScript Promises
- Constructing a Promise
- Handling the success or failure of a Promise
- Chaining multiple Promises
To get the most out of this article, readers should understand the fundamentals of JavaScript.
What is a Promise?
A Promise is a JavaScript object that represents the outcome of an asynchronous operation. When you work with asynchronous tasks like network requests or file reading, you can use Promises to handle the result of those tasks.
A Promise isn't the function itself, nor is it the direct result of the function. Instead, it acts as a container for a future value, which can be either a successful result or a failure (error).
The state of a Promise reflects the progress of the asynchronous operation:
- Pending (the operation is ongoing and hasn't completed yet)
- Resolved: (the operation is succesful)
- Rejected: (the operation has failed)
Please note that the failure of a Promise doesn't always indicate bad code. Failure can be due to external conditions not met, or explicit error handling within the code.
For example, the user of a digital library has picked a book that is not available. Based on the constructor, the promise may be rejected. The failure of this operation is due to low inventory, not bad code. Yet this condition must be explicitely stated in the promise constructor.
Constructing a promise object
The Promise constructor method uses what is known as an executor function. An executor function is simple a function that returns either a resolved, or a rejected value. In most cases, this function will return both values depending on the conditions set by the developer.
The syntax for constructing a promise is as follows:
const executorFunction = (resolve, reject) => {
//function body
if(condition){
resolve('This promise is succesful')
}
else {
reject('This promise has failed')
}
}
const newPromise = new Promise(executorFunction)
We could also use the convention of nesting a promise constructor inside a regular function. Like so:
const regularFunction = (argument) => {
return new Promise ((resolve, reject) => {
if(condition){
resolve('This promise is succesful')
}
else {
reject('This promise has failed')
}
})
}
Let us break it down:
- (resolve, reject): resolve and reject are functions that are built into the promise constructor. They handle cases of success, or failure
- body: the executor function body
- if, else: if a condition is met, resolve() is triggered. Otherwise, reject() is triggered.
- new Promise(): a promise is an object, and needs to be instantiated.
Example: Check if a book is available
//Create a database of books
const books = {
"Steve Jobs": {
title: "Steve Jobs",
author: "Walter Isaacson",
quantityInStock: 5,
libraryPointsRequired: 2,
},
"Elon Musk": {
title: "Elon Musk",
author: "Walter Isaacson",
quantityInStock: 1,
libraryPointsRequired: 2,
},
"Hard Drive": {
title: "Hard Drive",
author: "James Wallace",
quantityInStock: 3,
libraryPointsRequired: 2,
},
"The Innovators": {
title: "The Innovators",
author: "Walter Isaacson",
quantityInStock: 4,
libraryPointsRequired: 2,
},
"Einstein: His Life and Universe": {
title: "Einstein: His Life and Universe",
author: "Walter Isaacson",
quantityInStock: 2,
libraryPointsRequired: 2,
},
"The Code Breaker": {
title: "The Code Breaker",
author: "Walter Isaacson",
quantityInStock: 1,
libraryPointsRequired: 2,
},
"The Martian": {
title: "The Martian",
author: "Andy Weir",
quantityInStock: 5,
libraryPointsRequired: 2,
},
Artemis: {
title: "Artemis",
author: "Andy Weir",
quantityInStock: 2,
libraryPointsRequired: 2,
},
};
// Object holding a user profile
const user = {
name: "Armstrong Olusoji",
bookShelf: [],
};
// Write a function that checks if a book is in stock
const verifyOrder = (orderObject) => {
return new Promise((resolve, reject) => {
let book = null;
const title = orderObject.title;
const quantity = orderObject.quantity;
if (books.hasOwnProperty(title)) {
book = books[title];
}
if (book.quantityInStock >= quantity) {
console.log(`${book.title} by ${book.author} is in stock`);
resolve(book);
} else {
console.log(`${book.title} by ${book.author} is not in stock`);
reject(book);
}
});
};
Now, we can simply call verifyOrder, and depending on the availability of a book, it will either send a resolved value, or a reason for rejection.
But what happens after verifyOrder has run? Do we want to log the resolve/reject value to the console? Do we want to do something else? What do we do after a promise is resolved or rejected?
Handling success and failure with .then()
The resolve / reject value can be resolved simply by passing a callback function into .then()
Let us test this with our verifyOrder function:
const handleSuccess = (resolvedValue) => {
console.log(resolvedValue);
};
const handleFailure = (rejectedValue) => {
console.log(rejectedValue);
};
verifyorder("Hard Drive", 5).then(handleSuccess, handleFailure);
- handleSuccess(): is a callback function takes a single argument resolvedValue. In case of success, this function logs the resolved value to the console.
- handleFailure(): is a callback function that receives a single argument rejectedValue. In case of failure, this function logs logs the rejected value to the console.
- .then(): receives two callback functions as triggered. The first argument is triggerd if the promise is successful. The second argument is triggered if the promise fails.
- If verifyOrder succeeds, handleSuccess will execute. Otherwise, handleFailure will execute.
Please note that once you add .then() to any promise, the promise's resolved and rejected values are automatically passed to the corresponding callback function.
However, there are other ways to use the success/failure callback functions.
const handleSuccess = (resolvedValue) => {
console.log(resolvedValue);
};
const handleFailure = (rejectedValue) => {
console.log(rejectedValue);
};
checkAvailability("Hard Drive", 5).then(handleSuccess).then(handleFailure);
This code works the same way. The only difference is that rather than put both callback functions in a single .then(), we seperate the concerns. Like in the first method, the success handler needs to come first. But there is yet another way to achieve the same outcome.
const handleSuccess = (resolvedValue) => {
console.log(resolvedValue);
};
const handleFailure = (rejectedValue) => {
console.log(rejectedValue);
};
checkAvailability("Hard Drive", 5).then(handleSuccess).catch(handleFailure);
This method is similar to the second method. The only difference is that we use .catch() to handle failure rather than a second .then()
Either of these three methods will simply send the resolve / reject value from verifyOrder to the respective callback functions.
But what if, depending on the availability of a book, we want to trigger a new function? Let us talk about chaining promises.
Chaining Promises / Promise Composition
In the previous exercise, we created a promise named verifyOrder. This promise checks if the book a customer wants is in stock. If it is in stock, the promise is resolved, and the success handler is triggered. Otherwise, the failure handler will execute.
But what happens after that?
Well, if we verify that a book is available, we want to add that book to the user's bookshelf. But if it is not, we want to recommend other books from the same author.
Promise composition enables us to connect promises. In our example, a fulfilled verifyOrder promise will trigger the checkOut Promise. If checkOut is successful, the addToShelf promise will be triggered successfully. A rejected verifyOrder promise will trigger the recommendBook promise. Let us see this in practice.
First, we create a checkOut function to handle checkouttitle in. It should check the object order's libraryPoints. If there are enough library points, the promise should deduct the required points from libraryPoints, and then, title in that order to the user's bookShelf. Otherwise, it should be rejected.
const order = {
title: "The Innovators",
quantity: 3,
libraryPoints: 10,
};
const checkOut = (book) => {
return new Promise((resolve, reject) => {
const requiredPoints = book.libraryPointsRequired;
const title = book.title;
const points = order.libraryPoints;
if (requiredPoints <= points) {
order.libraryPoints -= requiredPoints;
console.log(
`The transaction is successful, and your library card now has ${order.libraryPoints} points`
);
resolve(user.bookShelf.push(`${book.title} by ${book.author}`));
} else {
reject("You don't have enough points for this transaction");
}
});
};
Finally, we will add a new promise called recommendBook. If verifyOrder fails, recommendBook will highlight other books from the same author.
const recommendBook = (book) => {
return new Promise((resolve) => {
const authorToMatch = book.author; // Use the author from the provided book
const recommendedBooks = [];
for (const title in books) {
const bookItem = books[title];
if (bookItem.author === authorToMatch && title !== book.title) {
recommendedBooks.push(bookItem.title);
}
}
console.log(
`We don't have the book you wanted, but here are some other books from the same author: ${recommendedBooks}`
);
resolve(recommendedBooks);
});
};
Finally, let us chain all these newly created promises together.
verifyOrder(order)
.then(checkOut)
.then((updatedBookShelf) => {
console.log(
`Your book has been added to the bookShelf: ${updatedBookShelf}`
);
})
.catch((error) => {
console.error(`An error occurred: ${error}`);
});
A breakdown of what is happening here is as follows:
- We call verifyOrder with the argument order
- If the verifyOrder promise resolves then checkOut should execute
- If checkOut succeeds, we chain an anonymous function to log the value of return value of checkOut. Something to note here is that .then automatically appends the return value of the previous promise as an arguement of the current promise.
- Finally, we use .catch() in case the promise fails at any point. Passing the 'error' argument will catch any error during the promise, and log it to the console. This could be a useful debugging tool!
Conclusion
Asynchronous operations are vital in web development. Not only do they improve performance, but they also help developers create logical flows for their functions to execute. Since JavaScript is a single-threaded language, we can build asynchronous operations with Promises.
Promises are simple to use. Creating one is as simple as using the 'new Promise' constructor. Since a promise is a function, we ought to add the arguments resolve, and reject. These arguments represent the return value of the promise if it fails or succeeds.
In the event of a success or a failure, we may want to trigger another function. We can use .then(() => success handler function), or .catch(() => failure handler function). In the spirit of asynchronous operations, these help us logically manage the flow of tasks.
Admittedly, promises have more features. However, discussing them would be out of the scope of this article. You can read more here
Posted on May 27, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.