Asynchronous operations, such as fetching data from external APIs or handling user interactions, introduce complexities that demand error handling strategies. In this guide, we will delve into the art of mastering asynchronous error handling, exploring best practices to fortify our applications against the onslaught of errors.
Try-Catch with Async/Await:
The dynamic duo of try-catch
blocks and async/await
syntax offers a robust solution for handling errors in asynchronous code. By encapsulating asynchronous operations within a try
block, developers gain the ability to gracefully catch and handle errors that may occur during execution.
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
throw error; // Re-throw the error to propagate it further
}
}
In this example, the try-catch
block envelops our asynchronous code, enabling us to capture and log errors that arise during the fetching of data. By re-throwing the error, we ensure that it propagates further up the call stack, facilitating comprehensive error handling throughout our application.
Promise.catch():
The Promise.catch()
method serves as a stalwart guardian against asynchronous errors, offering a concise solution for handling promise rejections. By appending a .catch()
clause to our promise chain, developers can intercept and handle errors that occur during the execution of asynchronous operations.
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => {
// Process the data
})
.catch((error) => {
console.error('Error fetching data:', error);
// Display a user-friendly error message
alert('An error occurred while fetching data.');
});
In this snippet, the .catch()
method intercepts any errors that occur during the fetching and processing of data, allowing developers to log the error for debugging purposes and provide users with a friendly error message.
Global Error Handling for Unhandled Promise Rejections:
In addition to local error handling techniques, for unhandled promise rejections, it is possible to also implement global error handling by using the unhandledrejection
event. By Using this event, developers can capture and handle promise rejections that occur without a corresponding rejection handler.
// Setup global error handling for Unhandled Promise Rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Additional logging or error handling can be added here
});
// Example of a Promise that is not handled
const unhandledPromise = new Promise((resolve, reject) => {
reject(new Error('This Promise is not handled'));
});
// Uncomment the line below to see the global error handling in action
// unhandledPromise.then(result => console.log(result));
// Example of a Promise with proper error handling
const handledPromise = new Promise((resolve, reject) => {
reject(new Error('This Promise is handled'));
});
handledPromise
.then(result => console.log(result))
.catch(error => console.error('Error:', error));
In this example, the unhandledrejection
event is utilized to log unhandled promise rejections globally, providing insight into any promises that are rejected without being handled appropriately. Additionally, two promises are demonstrated—one without proper error handling and one with a catch clause to handle errors gracefully.
Conclusion
In summary, handling errors, specifically asynchronous ones, in React applications requires a multi-faceted approach. By taking preventive measures, implementing global error handling, and communicating clearly with users, you can enhance the reliability and usability of your apps. Remember to prioritize simplicity in error messages and keep users informed. Stay updated with React's latest features and best practices to ensure your applications remain resilient and stable in the face of challenges.
Top comments (8)
Interesting post. Personally, I prefer the try catch approach. I try to avoid the
promise.then
syntax since it can be hard to understand the execution flow.I prefer a mix of both await and .then/.catch, since try-catch is usually outside of the promise chain, so you cannot pinpoint where the error happened unless you wrap every single promise in a try-catch-block, which breaks the reading flow:
Good point! Never thought about it.
You have a good point. Actually, depending on the context, each approach may offer distinct advantages.
I love this. Though would add a bit. You should handle process.on and things like sigterm. You should use a context of the call. Async local storage is your friend. Either promise or async (async is a promise under the hood) and don't mix with callbacks. Callback that returning rejected promise throwing an error is a nightmare. Don't catch any error and make sure that error leads to action. Often I prefer server to crash because this enforces user/operator to solve the issue while handled messages are often neglected (if it works then errors are ignored even if "payment failed" but you were able to pick item from the shop).
Now imagine the ugly code
I am not sure if the above works - just wanted to show how a nightmare can look like.
Oh... And it should fail in the delayed error handling failing to send headers that were already sent.
Thank you for introducing me to the
unhandledRejection
event.Glad to help!
Nice post thank you 👏
In browsers or JS runtimes that implement the Web API one could use
addEventListener("unhandledrejection", eventHandler)
.Also I often need to implement retry strategies for my async functions (in case of network error or else). I came up with a tiny utility called Tentative that simplifies the process: github.com/sim-la/tentative, but I'll be curious to hear what others use for handling retries in async functions.