Introduction
Asynchronous programming is a must-have in modern JavaScript development, allowing developers to perform non-blocking operations, such as fetching data from a server, reading files, or executing time-consuming operations.
JavaScript promises are a powerful feature introduced in ES6 (ECMAScript 2015) to manage asynchronous operations more efficiently and cleanly.
This blog post is a guide to JavaScript promises, offering a clear understanding how promises can be used to handle asynchronous tasks in an elegant way.
What Are JavaScript Promises?
A Promise in JavaScript is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value.
A Promise is a proxy for a value that is not necessarily known when the promise is created, this value can be evaluated at some point in the future.
Promises can be in one of three states:
- Pending: The initial state of a promise. The operation has not completed yet.
- Fulfilled: The operation completed successfully, and the promise has a resolved value.
- Rejected: The operation failed, and the promise has a reason for the failure.
Creating and Using Promises
To create a promise, you can use the Promise
constructor, which has one argument - a function called the executor.
The executor function is executed immediately by the Promise implementation and it has two function parameters: resolve
and reject
callbacks.
const promise = new Promise((resolve, reject) => {
try {
// Execute operation
resolve("Operation succeeded");
} catch (e) {
reject("Operation failed");
}
});
When a promise starts executing it's in the Pending state by default.
Later promise must be transferred to either Fulfilled or Rejected state.
resolve
callback is used to transfer a promise to Fulfilled state and return a resulting value of the promise as parameter.
reject
callback is used to transfer a promise to Rejected state and return a failure result of the promise as parameter.
There are 2 ways to get the promise result:
- using
then()
andcatch()
functions that hook to the promise - using
async/await
statements
Get Promise Result Using then() and catch() Functions
To get a result from a promise, you can call then()
function for success scenarios and catch()
function for handling failures.
For example:
promise
.then(result => {
// "Operation succeeded" if successful
console.log(result);
})
.catch(error => {
// "Operation failed" if an error occurred
console.error(error);
});
You can chain these methods to perform additional operations after the previous promise has completed.
function firstPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("First promise finished with result: 1");
resolve(1);
}, 1000);
});
}
function secondPromise(previousResult) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Second promise finished with result: ", previousResult + 1);
resolve(previousResult + 1);
}, 1000);
});
}
function thirdPromise(previousResult) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Third promise finished with result: ", previousResult + 1);
resolve(previousResult + 1);
}, 1000);
});
}
firstPromise()
.then(result => secondPromise(result))
.then(result => thirdPromise(result))
.then(finalResult => console.log("All promises completed with final result: ", finalResult))
.catch(error => console.error("An error occurred:", error));
Here we chain together 3 promises executing one after another. In this example "3" is printed to console as a final result.
The final catch()
function is called when an error occurs or reject()
function is called in any of 3 promises.
Before the invention of Promises performing asynchronous operations anticipated using callbacks.
This led to a bad code structure with deeply nested callbacks, especially when using multiple asynchronous operations one after another.
Here is how the previous example will look like when using callbacks:
function firstCallback(callback) {
setTimeout(() => {
console.log("First callback finished with result: 1");
callback(1);
}, 1000);
}
function secondCallback(previousResult, callback) {
setTimeout(() => {
console.log("Second callback finished with result: ", previousResult + 1);
callback(previousResult + 1);
}, 1000);
}
function thirdCallback(previousResult, callback) {
setTimeout(() => {
console.log("Third callback finished with result: ", previousResult + 1);
callback(previousResult + 1);
}, 1000);
}
firstCallback(function(result) {
secondCallback(result, function(newResult) {
thirdCallback(newResult, function(finalResult) {
console.log("All callbacks completed with final result: ", finalResult);
});
});
});
Doesn't look pretty, right? That's why the invention of promises was a huge step further in asynchronous JavaScript development.
But what if I told you - that it's not the best viable option nowadays? Let's explore what is the best option.
Get Promise Result Using async/await
async/await
statements in JavaScript are a complete game changer in asynchronous development.
ES2017 introduced async functions and the await
keyword, that allow developers to write asynchronous code that looks and behaves a like synchronous code:
async function fetchDataAsync() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to fetch data: ", error);
}
}
const data = await fetchDataAsync();
console.log(data);
Here an async
function returns a Promise<T>
, that holds a data received from the API call.
By using await
keyword we get this data as a promise result.
After the line const data = await fetchDataAsync();
we can simply write more code as if all operations were executed synchronously.
It looks perfect!
Now let's explore how we can update one of the previous examples where we chained 3 promises with a then()
function, when using async/await
statements:
function firstPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
}
function secondPromise(previousResult) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(previousResult + 1);
}, 1000);
});
}
function thirdPromise(previousResult) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(previousResult + 1);
}, 1000);
});
}
try {
const firstResult = await firstPromise();
console.log("First promise finished with result: ", firstResult);
const secondResult = await secondPromise(firstResult);
console.log("Second promise finished with result: ", secondResult);
const thirdResult = await thirdPromise(secondResult);
console.log("Third promise finished with result: ", thirdResult);
console.log("All promises completed with final result: ", thirdResult);
} catch (error) {
console.error("An error occurred:", error);
}
It looks really nice. This code is much more readable and maintainable because of using the async/await
statement.
Summary
JavaScript promises are a vital feature for performing non-blocking operations, such as fetching data from a server, reading files, or executing time-consuming operations.
It offers a more manageable and readable approach compared to old callback-based solutions.
You can use the async/await
syntax to write even cleaner and more efficient JavaScript code.
Hope you find this blog post useful. Happy coding!
Originally published at https://antondevtips.com.
After reading the post consider the following:
- Subscribe to receive newsletters with the latest blog posts
- Download the source code for this post from my github (available for my sponsors on BuyMeACoffee and Patreon)
If you like my content — consider supporting me
Unlock exclusive access to the source code from the blog posts by joining my Patreon and Buy Me A Coffee communities!
Top comments (0)