If you’re familiar with what promises are, you can skip the next section and jump straight to the approaches. Otherwise, read on!
What are Promises?
The word “Promise” itself describes the concept of a commitment, signifying that something will occur in the future. A promise is a value that may not be available immediately but could be in the future.
In JavaScript, a promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. The program’s execution does not halt while waiting for the promise to resolve; instead, it continues to execute the next line of code. The promise will be resolved or rejected at a later point. For example, a promise can be used to fetch data from an API. The promise will be resolved when the data is available, and the program can continue its execution. If an error occurs, the promise will be rejected, and the error can be handled appropriately.
The promise object has three states: Pending, Fulfilled, and Rejected. Consequently, handling the fulfilled and rejected states of the promise is crucial to maintaining the smooth execution of our program.
There are various ways to handle promises in JavaScript. In this article, we’ll explore some of the most common approaches to handling data and errors in promises, discussing the pros and cons of each approach. Let’s get started!
Initial Setup
Suppose we have a function that returns a promise named sayHi
. This function takes a boolean parameter called shouldReject
. If shouldReject
is true
, the function returns a rejected promise; otherwise, it returns a resolved promise.
function sayHi(shouldReject) {
return new Promise((resolve, reject) => {
if (shouldReject) {
reject("No, I won't say Hi! 😡");
}
resolve("Hi! 😊");
});
}
Approach 1: Using then
/catch
The first approach is to use the then
/catch
method. The then
method handles the resolved data (usually), while catch
deals with errors in promises. But both resolved data and errors can be handled in a single call using the then
method.
sayHi().then(
(result) => console.log(result),
(error) => console.log(error)
);
Hi! 😊
This approach is concise and convenient for simple scenarios. However, for complex chains with multiple promises, separate then
and catch
calls can enhance code readability and maintainability. This separation improves code organization, making success and error-handling logic clearer. For instance:
sayHi(true)
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error);
});
No, I won't say Hi! 😡
While the then
/catch
method is common, it has some drawbacks. For instance, if multiple promises are used, the catch
block captures errors from any point in the promise chain, leading to a lack of granularity in error handling.
sayHi(true)
.then((result) => {
console.log(result);
})
.then(() => {
console.log("This will not be executed");
throw new Error("This will not be caught by the next catch block");
})
.catch((error) => {
console.log(error);
});
No, I won't say Hi! 😡
This problem can be mitigated by using the catch
method on each promise, but this results in more verbose code. Nesting too many then
/catch
blocks can also lead to “promise hell,” making the code challenging to read and maintain.
sayHi(true)
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error);
})
.then(() => {
console.log("This will be executed");
throw new Error("This will be caught by the next catch block");
})
.catch((error) => {
console.log(error.message);
});
No, I won't say Hi! 😡
This will be executed
This will be caught by the next catch block
While then
and catch
are still widely used, some developers prefer the cleaner and more concise syntax of async
/await
for handling promises, especially in scenarios where a more synchronous-looking code is desired.
Approach 2: Using async
/await
The second approach involves using async
/await
. The async
keyword defines an asynchronous function, while await
is used to wait for a promise to resolve. The await
keyword can only be used inside an async
function and will pause the function’s execution until the promise is resolved. It also catches any errors thrown by the promise, which can be handled using a try
/catch
block.
async function asyncWrapper() {
try {
const result = await sayHi();
console.log(result);
} catch (error) {
console.log(error);
}
}
asyncWrapper();
Hi! 😊
The async
/await
syntax can be combined with the catch
method syntax:
const message = await sayHi(true).catch((error) => {
console.log(error);
});
console.log(message);
No, I won't say Hi! 😡
However, a drawback is that if a rejection occurs, the execution of the next line where message
is being used cannot be stopped. Additionally, outside the catch
block, there is no access to the error
object, making it impossible to check for errors before the execution of the next line.
Despite these limitations, the async
/await
syntax is more concise and easier to read than the then
/catch
syntax. It allows for more granular error handling using the try
/catch
block and includes a finally
block to execute code after the promise is resolved or rejected.
async function asyncWrapper() {
try {
const result = await sayHi(true);
console.log(result);
} catch (error) {
console.log(error);
} finally {
console.log("This will be executed regardless of the promise's state");
}
}
asyncWrapper();
No, I won't say Hi! 😡
This will be executed regardless of the promise's state
We can also go with kinda callback
pattern we’re used to within Node.js. We can get both the data and the error from a single call and can easily handle them.
async function asyncWrapper(promise) {
try {
const result = await promise;
return [result];
} catch (error) {
return [, error];
}
}
const [message, error] = await asyncWrapper(sayHi());
if (error) {
console.log(error);
} else {
console.log(message);
}
Hi! 😊
Note:
null
orundefined
can be returned instead of an empty array, for example,return [result, null]
orreturn [result, undefined]
. Another approach utilizing thePromise.allSettled
method will be discussed shortly.
Approach 3: Using Promise.allSettled
Concurrent promises can be handled using the Promise.all
and Promise.allSettled
methods. Promise.allSettled
is used to wait for all promises to be settled. It takes an array of promises as an argument and returns a promise that is resolved when all promises are settled. The promise returned includes an array of objects, each with a status
property and
a value
when fulfilled or a reason
when rejected.
Promise.allSettled([sayHi(true), sayHi(false)]).then((results) => {
console.log(results);
});
[
{ status: 'rejected', reason: 'No, I won\'t say Hi! 😡' },
{ status: 'fulfilled', value: 'Hi! 😊' }
]
If promises depend on each other, and immediate rejection is desired if any fails, Promise.all
can be used. This method waits for all promises to be resolved and is rejected immediately if any promise is rejected. Errors can be handled using the catch
method.
Promise.all([sayHi(false), sayHi(false), Promise.resolve("Resolved!! 😁")])
.then((results) => {
console.log(results);
})
.catch((error) => {
console.log(error);
});
[ 'Hi! 😊', 'Hi! 😊', 'Resolved!! 😁' ]
Promise.all([sayHi(true), sayHi(false), sayHi(false)])
.then((results) => {
console.log(results);
})
.catch((error) => {
console.log(error);
});
No, I won't say Hi! 😡
The async
/await
syntax for handling data and errors in a single call can be achieved using the Promise.allSettled
method:
function asyncWrapper(promise) {
return Promise.allSettled([promise]).then(([{ value, reason }]) => [
value,
reason,
]);
}
const [message, error] = await asyncWrapper(sayHi());
if (error) {
console.log(error);
} else {
console.log(message);
}
Hi! 😊
This approach appears more concise and readable than the async
/await
syntax!
Other methods such as
Promise.race
andPromise.any
exist to handle concurrent promises. You can read more about these here.
Conclusion
In this article, we explored different ways to handle errors in JavaScript promises, discussing the pros and cons of each approach. I hope you found this article useful. Thanks for reading! 😊