DEV Community

Benjamin Raffetseder
Benjamin Raffetseder

Posted on

Simplifying Async Error Handling in TypeScript

If you've worked with asynchronous code in TypeScript for any length of time, you'll know that error handling can get messy fast. Between nested try-catch blocks and repetitive error-handling logic, it doesn't take long before your codebase starts feeling cluttered. That's why I've found myself leaning toward a more elegant solution — the tryCatch helper function. This little utility is inspired by Go's error-handling approach and wraps your async functions in a way that simplifies error management, making your code more readable and maintainable.

The Code

Let's jump right into the tryCatch function:

export const tryCatch = async <T>(fn: () => Promise<T>, logError = false): Promise<[T | undefined, Error | undefined]> => {
  try {
    const data = await fn();
    return [data, undefined];
  } catch (error: unknown) {
    const err = error instanceof Error ? error : new Error(String(error));

    if (logError) {
      console.error(err);
    }

    return [undefined, err];
  }
}
Enter fullscreen mode Exit fullscreen mode

Why I Use This Approach

As someone who's spent a lot of time dealing with asynchronous code, I've come to really appreciate the benefits tryCatch brings to the table:

  1. Cleaner Code: One of the biggest advantages is that tryCatch helps reduce the number of try-catch blocks scattered throughout your code. This may seem like a small thing, but when you're managing multiple async operations, it makes a big difference in keeping your code readable and manageable. Instead of having multiple try-catch blocks everywhere, you deal with both success and error cases in a predictable, clean way. And as a bonus, this approach makes it easier for others to read your code and understand what's going on.

  2. Consistent Error Handling: One thing that I've learned over the years is that consistent error handling is key to a maintainable codebase. With tryCatch, you establish a clear and uniform pattern for handling errors across your code. This not only reduces mental overhead but also makes your codebase easier to maintain as it scales.

  3. Optional Logging: Sometimes you want to log errors right away, sometimes you don't. The optional logError flag gives you the flexibility to toggle error logging on or off, depending on what makes sense for the situation. This keeps your code free from repetitive console logs calls and allows for centralized logging behavior.

  4. Type Safety: Since we're working with TypeScript, type safety is always on my mind. tryCatch ensures that if an error occurs, the result will be undefined, and vice versa. This reduces the chances of those frustrating runtime bugs that crop up because of type mismatches. Plus, it's a nice reassurance when you're juggling complex data flows.

Example Usage:

   const [data, error] = await tryCatch(async () => {
     // Your async logic here
   }, true);
   if (error) {
     // Handle the error
   } 
   if (!data) {
    // Handle the absence of data
   }
   // Continue with the data
Enter fullscreen mode Exit fullscreen mode

Why It's Better Than Traditional try-catch

I've seen plenty of projects where traditional try-catch blocks work well—at first. But as the project grows and async operations multiply, they start to become a headache. Here's how tryCatch can make things easier:

  • No Deep Nesting: Once you start nesting multiple try-catch blocks, it's easy to lose track of what's happening where. tryCatch helps keep your code flatter, which makes the logic easier to follow.

  • Single Responsibility: This function allows you to encapsulate error handling in a single place. I like to think of it as aligning with the single responsibility principle — your async functions focus on what they're supposed to do, while tryCatch handles the errors. This separation makes the code easier to read and maintain.

  • Focus on the Happy Path: One of my favorite things about using tryCatch is how it allows me to focus on the happy path in my code. Instead of constantly breaking the flow to deal with errors, I can keep my main logic straightforward and handle the errors separately. This small shift can make a big difference when you're trying to keep the overall intent of your code clear.

Final Thoughts

As someone who's been building with TypeScript for a while now, I've found tryCatch to be a real asset in managing asynchronous error handling. It's one of those small changes that have a surprisingly big impact, especially when you're working in a large or growing codebase. By reducing noise and ensuring consistent error handling, tryCatch helps me write cleaner, more maintainable code.

If you haven't tried it yet, give it a go — it might just make your life a little easier too.

Top comments (0)