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
}
}
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);
}
}
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))];
}
};
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))];
}
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();
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.