Zig's Catch Pattern In JavaScript

How It Started

I was watching injuly's live video stream in the discord server Zig India Discord just for the vibe. He was writing something in Zig - can't remember what - but what caught my attention was Zig's error handling using try and catch. I liked the way it handled errors and immediately wanted something similar in JavaScript.

Inspiration

Let's take a look at the Zig's error handling with catch with an good example.

const std = @import("std");

fn divide(a: i32, b: i32) !i32 {
    if (b == 0) {
        return error.DivisionByZero;
    }
    return a / b;
}

pub fn main() !void {
    // Example 1: Using catch to provide a default value
    const result1 = divide(10, 0) catch 0;
    std.debug.print("Result with default: {}\n", .{result1});  // prints 0

    // Example 2: Using catch to handle the error
    const result2 = divide(10, 0) catch |err| {
        std.debug.print("Oops! {}\n", .{err});
        return err;
    };
}

Here, We handle the error at the same place where the function is called. No new scope, no bulky try-catch block and no extra indentation. It's easy to read and understand.

Zig's influence

I really wanted something similar in JavaScript. I tried creating classes and utility functions, but nothing worked -until I realized we already have something similar in JavaScript, it was staring me right in the face.

To show Zig's way of error handling in JavaScript, I'm going to do something unconventional: I'll make the divide() function asynchronous (now you know why good was stricken out). I'll come back to this later.

Here you go:

async function divide(a, b) {
    if (b === 0) {
        throw new Error("DivisionByZero");
    }
    return a / b;
}

async function main() {
    // Example 1: Using a default value with .catch()
    let result1 = await divide(10, 0).catch((_) => 0);
    console.log(`Result with default: ${result1}`); // prints 0

    // Example 2: Handling the error explicitly
    const result2 = await divide(10, 0).catch((err) => {
      console.log(`Oops! ${err.message}`);
      throw err;
    });
    console.log(`Result: ${result2}`);
}

main();

I did not use a try-catch block at all, and it looks very similar to Zig's catch block. Practically, it's the same thing.

Making the divide() function asynchronous isn't ideal or recommended;1 this was just to illustrate the concept. Most of the time, we (backend people) deal with asynchronous functions anyway.

Another way to do it is to use a utility function.

async function $try<T extends (...args: any[]) => any>(
  fn: T
): Promise<ReturnType<T>> {
  return await fn()
}

We can use it like this:

let _ = await $try(() => divide(10, 1)).catch((_) => 0);

But as you can see, it clutters the call site, making it hard to read.

What about synchronous functions then?

We can use following utility function which has the same API as the above one:

const $try = <T extends (...args: any[]) => any>(fn: T) => {
  const Catch = class {
    constructor(private readonly value: T) { }

    catch(fn: (err: unknown) => ReturnType<T> | never): ReturnType<T> {
      try {
        return this.value()
      } catch (err) {
        return fn(err)
      }
    }
  }

  return new Catch(fn)
}

try is a reserved keyword in JavaScript, we have to settle for $try or try_-whichever suits you. $try() returns an anonymous class with a catch() method that takes a synchronous error handler.

function divide(a, b) {
    if (b === 0) {
        throw new Error("DivisionByZero");
    }
    return a / b;
}

function main() {
    // Example 1: Using a default value with .catch()
    let result1 = $try(() => divide(10, 0)).catch((_) => 0);
    console.log(`Result with default: ${result1}`); // prints 0

    // Example 2: Handling the error explicitly
    const result2 = $try(() => divide(10, 0)).catch((err) => {
      console.log(`Oops! ${(err as any).message}`);
      throw err;
    });
    console.log(`Result: ${result2}`);
}

main();

I do prefer to handle errors at the call site, but I like to experiment with different styles to see if they make code more readable.

Cheerio!

Don't Lose the Rhythm

Footnotes

  1. Well, It depends, If you have huge synchronous task, you don't wanna hog the event-loop, you might consider to divide the work into smaller chunks and handle them asynchronously. I just don't wanna make synchronous functions asynchronous it's misleading.