DEV Community

Cover image for Handling Asynchronous Errors Like a Pro
Lisandra
Lisandra

Posted on • Edited on

Handling Asynchronous Errors Like a Pro

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 tryblock, 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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.');
  });
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
atscub profile image
Abraham Toledo

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.

Collapse
 
lexlohr profile image
Alex Lohr

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:

const data = await fetch(url)
  .catch((error) => ({ msg: "request failed", error })
  .then(r => r.json())
  .catch((error) => ({ msg: "request malformed", error }));
Enter fullscreen mode Exit fullscreen mode
Collapse
 
atscub profile image
Abraham Toledo

Good point! Never thought about it.

Collapse
 
lisichaviano profile image
Lisandra

You have a good point. Actually, depending on the context, each approach may offer distinct advantages.

Collapse
 
adderek profile image
Maciej Wakuła • Edited

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

server.listen('/fail', (req, res) =>{
   return new Promise(resolve=>{
       resolve(() =>{
           res.send(()=>{
               return Promise.resolve(() =>
                  process.on('unhandledError', () =>
                     throw new Error(() =>res.set('error', true)
                  )
                  return true
           })
       } ) 
   }) 
}) ;
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
citronbrick profile image
CitronBrick

Thank you for introducing me to the unhandledRejection event.

Collapse
 
lisichaviano profile image
Lisandra

Glad to help!

Collapse
 
sim-la profile image
SL

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.