DEV Community

Cover image for Stop Repeating Try/Catch: This TypeScript Wrapper Makes Error Handling Easy
Angel Serrano
Angel Serrano

Posted on

Stop Repeating Try/Catch: This TypeScript Wrapper Makes Error Handling Easy

I bet you have this code in your project code base

async function getUserData(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    console.log("User data:", data);
    return data;

  } catch (error) {
    console.error("Failed to fetch user data:", error);
    // Handle or rethrow
  }
}

Enter fullscreen mode Exit fullscreen mode

Or cases like this one

import { MongoClient } from "mongodb";

async function connectToDb() {
  const uri = "mongodb://localhost:27017";
  const client = new MongoClient(uri);

  try {
    await client.connect();
    console.log("Connected to MongoDB!");
    const db = client.db("myApp");
    return db;
  } catch (error) {
    console.error("MongoDB connection failed:", error);
  }
}

Enter fullscreen mode Exit fullscreen mode

What similarities do these two pieces of code share?

  • Both use promises

  • Both use error handling using try/catch

What is the problem here?
The problem with this code is that it is very repetitive and verbose. It adds more lines of code to our codebase, and we are essentially repeating the same process over and over again.

Therefore, one solution for this case is to create a wrapper function for the try/catch code.

For the new ones

A wrapper function basically is a function that calls another function. It can be used to add more functionality to the existing function without modifying it, for example, to handle errors or logging purposes.

Solution

The wrapper function will be like this

export const safeAwait = async <T>(
  promise: Promise<T>
): Promise<[T | null, Error | null]> => {
  try {
    const data = await promise;
    return [data, null];
  } catch (error) {
    return [null, error instanceof Error ? error : new Error(String(error))];
  }
};
Enter fullscreen mode Exit fullscreen mode

Let’s break down this code and describe its typing

export const safeAwait = async <T>( promise: Promise<T> ): Promise<[T | null, Error | null]>

This is a function that accepts a promise of whatever type and returns a tuple of [whatever type or null, Error type or null]


Then inside we have the try/catch block where we await the promise execution, and if everything goes well, we return a tuple with the [data, null]

In case an error occurs, we return the same tuple now with an error instance [null, error]

try { 
  const data = await promise; 
  return [data, null]; 
} catch (error) { 
  return [null, error instanceof Error ? error : new Error(String(error))]; 
}
Enter fullscreen mode Exit fullscreen mode

Here we have an implementation of our function wrapper

import { safeAwait } from './safeAwait';

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

const fetchPosts = async (): Promise<Post[]> => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  return response.json();
};

const fetchData = async () => {
  const [data, error] = await safeAwait(fetchPosts());
 // check if error is not null
  if (error) {
    console.error('Error fetching data', error);
    return;
  }

  return data;
};

fetchData();
Enter fullscreen mode Exit fullscreen mode

Benefits

The main benefits of having this wrapper function are:

  • Less boilerplate

  • Explicit error path

  • Easier to compose and test

  • Code is more readable

This pattern helped me write cleaner and more maintainable async code, and I hope it helps you too.

Check out the full code on github
https://github.com/angeldavid218/trycatch-wrapper

See you in the next one!

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.