I don't like try/catch blocks. The syntax is ugly, they make guard clauses more difficult than they need to be. Structurally they often get in the way and produce confusing code. For any given function I would rather read it procedurally.
Let's take for example an endpoint that requires us to search for some related data before making an update. We first need to parse a request body, then find the item, then update it. All three of these actions may or may not throw an error. The code might look something like this.
1let parsed: ParsedType;
2
3try {
4 parsed = parse(request.body);
5} catch {
6 return 'Failed to parse'
7}
8
9let item: Item
10
11try {
12 item = await db.findItem({where: {id: parsed.id}});
13} catch {
14 return 'Item not found'
15}
16
17try {
18 return db.updateRelatedItem(...)
19} catch {
20 return 'Failed to update'
21}
A very simple example that can quickly get out of control the longer the logic gets. Zod already has built in functional error handling, so we can parse the request body without try/catch already. This is already a small improvement to me. We handle the error first, via a guard clause rather than catching it. For our db queries, there's no such feature. That's why I wrote a tryCatch utility function. The result of which gives us this.
1const parsed = bodySchema.safeParse(request.body)
2
3if (!parsed.success) {
4 return 'Failed to parse'
5}
6
7const { data: body } = parsed
8
9const findResults = await tryCatchAsync(() => {
10 return db.findItem({where: {id: body.id}})
11})
12
13if (!findResults.isSuccess) {
14 return 'Item not found'
15}
16
17const { data: item } = findResults
18
19const results = await tryCatchAsync(() => {
20 return db.updateRelatedItem(...)
21})
22
23if (!results.isSuccess) {
24 return 'Failed to update'
25}
26
27return results.data
And of course the code for this utility function with both a sync and async version can be found in my util library.
1type TryCatchResult<Type> =
2 | { data: Type; isSuccess: true }
3 | { error: unknown; isSuccess: false };
4
5export function tryCatch<T extends () => ReturnType<T>>(
6 function_: T,
7): TryCatchResult<ReturnType<T>> {
8 try {
9 return { data: function_(), isSuccess: true };
10 } catch (error) {
11 return { error, isSuccess: false };
12 }
13}
14
15export async function tryCatchAsync<
16 T extends () => Promise<Awaited<ReturnType<T>>>,
17>(function_: T): Promise<TryCatchResult<Awaited<ReturnType<T>>>> {
18 try {
19 const data = await function_();
20 return { data, isSuccess: true };
21 } catch (error) {
22 return { error, isSuccess: false };
23 }
24}