Use try/catch the right way

Jawad Ali - Aug 26 - - Dev Community

Asynchronous programming is an essential part of modern JavaScript development, especially when working with APIs, databases, or any other blocking operations. While JavaScript provides powerful tools like Promises and async/await for handling these operations we often use try-catch blocks to handle errors. Consider the following example:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}
Enter fullscreen mode Exit fullscreen mode

This pattern works well, but it becomes repetitive when we have multiple asynchronous operations throughout our codebase. Repeating try-catch blocks can lead to:

  • Code Clutter: The more we repeat this pattern, the more cluttered our code becomes.
  • Inconsistent Error Handling: Manually handling errors in multiple places increases the risk of inconsistencies.

Solution
Let's create a utility wrapper function to make our code cleaner and more maintainable.

The idea is to encapsulate the try-catch logic into a reusable function that can be applied to any promise-based operation. This function takes a promise as its input and returns an array with two elements:

  • The first is an error which returns null if the promise resolves successfully or the error if the promise rejects
  • The second is the resolved value if the promise resolved or null if the promise is rejected.

Let’s name this simple wrapper safeAsync

async function safeAsync(promise) {
  try {
    const resp = await promise;
    return [null, resp];
  } catch (error) {
    return [error, null];
  }
}
Enter fullscreen mode Exit fullscreen mode

This function accepts promise as input, uses try-catch to handle any errors, and returns a tuple [error, data].

Usage Example
Let’s explore how we can use safeAsync in different scenarios.

Simple Promise
Imagine we have a simple promise that resolves or rejects based on a condition:

const s1 = 'promise1';
const promise = new Promise((resolve, reject) => {
  if (s1 === 'promise2') {
    resolve({
      message: "It's valid",
      code: 200
    });
  } else {
    reject({
      message: "It's not valid",
      code: 400
    });
  }
});

(async () => {
  const [error, data] = await safeAsync(promise);
  if (error) {
    console.error(error);
    return;
  }
  console.log(data);
})();
Enter fullscreen mode Exit fullscreen mode

Output:
{ message: "It's not valid", code: 400 }

In this example, safeAsync simplifies the process of handling potential errors without needing to write a try-catch block manually.

Use with API Call
We can use safeAsync with API calls to keep our code clean and readable:

(async () => {
  const [error, data] = await safeAsync(fetch('https://jsonplaceholder.typicode.com/todos/1'));
  if (error) {
    console.error('API error:', error);
    return;
  }
  const jsonData = await data.json();
  console.log('API data:', jsonData);
})();
Enter fullscreen mode Exit fullscreen mode

Output:
API data: { userId: 1, id: 1, title: 'delectus aut autem', completed: false }

Here, safeAsync is used to manage the API call, catching any potential network errors and ensuring consistent error handling.

Multiple Asynchronous Operations
safeAsync can also help when dealing with multiple asynchronous operations in sequence:

(async () => {
  const [error1, data1] = await safeAsync(fetch('https://jsonplaceholder.typicode.com/todos/1'));
  const [error2, data2] = await safeAsync(fetch('https://jsonplaceholder.typicode.com/todos/2'));

  if (error1 || error2) {
    console.error('Error fetching data:', error1 || error2);
    return;
  }

  const jsonData1 = await data1.json();
  const jsonData2 = await data2.json();
  console.log('Data 1:', jsonData1);
  console.log('Data 2:', jsonData2);
})();
Enter fullscreen mode Exit fullscreen mode

**Output:*
Data 1: { userId: 1, id: 1, title: 'delectus aut autem', completed: false }
Data 2: { userId: 1, id: 2, title: 'quis ut nam facilis et officia qui', completed: false }

This approach allows us to handle each asynchronous operation independently while maintaining clean and consistent error-handling logic.

Conclusion
The safeAsync function offers a streamlined approach to managing asynchronous operations, enhancing code clarity and maintainability by eliminating repetitive try-catch blocks. This utility ensures consistent error handling across our application, minimizing the risk of bugs due to error mishandling. Its flexibility allows it to be applied to any promise-based activity, from simple operations to complex API calls. Additionally, safeAsync can be easily extended to incorporate notifications for both success and failure scenarios within the same wrapper, further centralizing and simplifying response management.

.
Terabox Video Player