Table of Contents
What is a promise in JavaScript ?
A promise in JavaScript is an object representing the eventual completion or failure of an asynchronous operation. Since most people are consumers of already-created promises, this guide will explain consumption of returned promises before explaining how to create them.
A promise is an object that indicates if an asynchronous operation is eventually complete successfully or failed. It also carry the result of the operation. There are four stages of a promise.
- fulfilled: Action related to the promise succeeded.
- rejected: Action related to the promise failed.
- pending: Promise is still pending i.e. not fulfilled or rejected yet.
- settled: Promise has been fulfilled or rejected.
JavaScript provides methods to handle these stages. We will discuss these methods later in this post.
Why do we need promises in JavaScript?
Promises provide a robust way to wrap the result of asynchronous work, overcoming the problem of deeply nested callbacks. Nested callbacks, also known as callback hell or the pyramid of doom, refers to a situation in JavaScript where multiple asynchronous operations are chained together using callbacks within callbacks, leading to deeply nested and indented code. This can make the code difficult to read, understand, and maintain.
asyncOperation1((error, result1) => {
if (error) {
console.log('Error:', error);
} else {
asyncOperation2(result1, (error, result2) => {
if (error) {
console.log('Error:', error);
} else {
asyncOperation3(result2, (error, result3) => {
if (error) {
console.log('Error:', error);
} else {
// ...more nested callbacks...
}
});
}
});
}
});
Promises alleviate callback hell by providing a more readable and structured approach to asynchronous code. They enable a sequential and chainable syntax, improving code readability. Promises centralize error handling, allowing a single .catch()
to handle errors for multiple promises, simplifying error management. Promises support composition, facilitating the chaining of asynchronous operations without excessive nesting. They offer better flow control with methods like Promise.all()
and Promise.race()
. Promises automatically propagate errors through the chain, reducing the need for error handling in each callback. Together, these features make code more organized, maintainable, and easier to reason about..
How to utilize stages of a promise
1. using then(), catch() and finally methods
JavaScript provides three methods to handle the above mentioned stages of a promise. They are:
- then()
This method is used to handle the resolved state of a Promise. It takes a callback function as an argument that will be executed when the Promise is successfully resolved.
- catch()
This method is used to handle the rejected state of a Promise. It takes a callback function as an argument that will be executed when the Promise is rejected.
- finally()
This method is used to specify a callback function that will be executed regardless of whether the Promise is resolved or rejected
These methods are commonly used in a “chain” that is connected with the dot (“.”) operator.
Let’s assume that there is a promise called “myPromise” returned from a certain API. How would you use the above mentioned function with this incoming promise in your code
myPromise
.then((result) => { console.log('Resolved:', result); })
.catch((error) => { console.log('Rejected:', error.message); })
.finally(() => { console.log('Promise execution completed.'); });
Let’s talk more about the then()
method. The then()
method takes two arguments.
- A callback function to handle the resolved state of the Promise
- A callback function to handle the rejected state of the Promise
myPromise.then( callback_resolved_state(), callback_rejected_state );
Because of this, you can actually handle all the states of a promise just by using then() method.
myPromise
.then(
// handle the resolved state of the Promise
(result) => {
console.log('Resolved:', result);
},
//handle rejected state of the Promise
(error) => {
console.log('Rejected:', error.message);
}
)
//replicate the functionality of finally method
.then(() => { console.log('Promise execution completed.'); });
2. using async/await
Another way to handle promise are using async
and await
keywords introduce in ES2017(ES8) version. Using these keywords , you can write more synchronous style of writing asynchronous code. This makes it easy read and understand the code.
async function myAsyncFunction() {
try {
const result = await myPromise; // Wait for the promise to resolve
console.log('Resolved:', result);
} catch (error) {
console.log('Rejected:', error.message);
} finally {
console.log('Promise execution completed.');
}
}
myAsyncFunction();
the async
keyword is used to define an asynchronous function. Inside the function, await is used to pause the execution and wait for the promise (myPromise) to resolve. The try/catch
block is used to handle both resolved and rejected states, and the finally block is used for finalization.
One of the main benefit of using async/await
syntax is that you can avoid using chaining as is the case with then()/catch()/finally() methods.
async function myAsyncFunction() {
try {
const result1 = await asyncOperation1();
console.log('Result 1:', result1);
const result2 = await asyncOperation2();
console.log('Result 2:', result2);
// ... more async operations ...
} catch (error) {
console.log('Error:', error.message);
}
}
myAsyncFunction();
asyncOperation1()
and asyncOperation2()
in the code above are asynchronous functions that return promises. With async/await
, the code inside the myAsyncFunction()
appears more linear and readable, resembling synchronous code.By using await, each asyncOperation
is awaited in sequence, and any errors are caught by the surrounding try/catch
block. This eliminates the need for explicit chaining and allows for a more straightforward and intuitive coding style.
The above mentioned methods and keywords are the common ways of resolving promises. However, they are not the only ways. You can also use Promise.all
method and Promise.race handle multiple promises simultaneously.
When to use catch/then/finally
- You prefer having finer and more precise control over the handling of promises at different stages of their lifecycle.
- You need to handle promises in a more functional and chaining-oriented style.
- You want to handle promises without having to define an
async
function. - You want to support older environments that may not have support for
async
/await
.
When to use async/await
- You prefer a more synchronous and linear coding style for handling promises.
- You want to write asynchronous code that resembles synchronous code, improving readability and maintainability.
- You need to handle promises within an
async
function, taking advantage of the simplified error handling withtry
/catch
. - You want to leverage the syntactic sugar provided by
async
/await
for handling promises.
Wrapping up
JavaScript promises provide a powerful and standardized mechanism for handling asynchronous operations. Promises improve code readability and maintainability by offering a sequential and chainable syntax, avoiding the pitfalls of callback hell. They provide centralized error handling, better flow control, and support for composition, making it easier to manage and coordinate multiple asynchronous tasks. Promises, along with the introduction of async
/await
, have greatly simplified asynchronous programming in JavaScript, enabling developers to write asynchronous code in a more synchronous and intuitive manner. By leveraging promises, developers can create more structured, modular, and error-resistant code, resulting in more efficient and reliable JavaScript applications.