DEV Community

Bruno Bernard
Bruno Bernard

Posted on

Simplify Error Handling with "Try": A JavaScript Library Inspired by Rust and Golang

Error handling in JavaScript has often been a source of frustration for developers. Unlike languages like Rust and Golang, where errors are explicit and treated as values, JavaScript tends to rely on implicit error handling. This can lead to difficulties in tracking down errors, especially when working with external libraries or integrating your own code.

The Problem with Implicit Errors in JavaScript

Consider the following JavaScript code:

import { fetchUser } from "./user";
import { leftpad } from "leftpad";

await fetchUser(); // Should we handle the errors?
leftpad(); // Should we handle the errors?
Enter fullscreen mode Exit fullscreen mode

In this snippet, errors are not explicit. There's no clear indication of how to handle errors, and your code essentially relies on trust alone. This lack of clarity can lead to unforeseen issues, making it challenging to build robust and reliable applications.

Introducing "Try": A Solution Inspired by Rust and Golang

The "Try" library aims to address the challenges of error handling in JavaScript by borrowing concepts from Rust and Golang. It introduces a structured approach where errors are treated as values, making them explicit and easier to manage.

Error Handling in Rust:

fn main() {
    let result = File::open("hello.txt");

    let greeting_file = match result {
        Ok(file) => file,
        Err(error) => // handle errors,
    };
}
Enter fullscreen mode Exit fullscreen mode

Error Handling in Golang:

f, err := os.Open("filename.ext")
if err != nil {
  // handle errors
}
Enter fullscreen mode Exit fullscreen mode

Why Use "Try" in JavaScript?

Errors in JavaScript can be tricky to handle due to their implicit nature. The "Try" library serves as a wrapper for your functions, enforcing the need to handle errors. Instead of errors being thrown, they are returned as values. This approach not only makes error handling more explicit but also aims to eliminate callback hell (nested then) and the tower of doom (try-catch block).

By treating errors as values, "Try" simplifies error handling, reducing the chances of overlooking errors and making your code more robust and maintainable.

Getting Started with "Try"

To get started with the "Try" library, follow these simple steps:

Available on Github

bun add @eznix/try
yarn install @eznix/try
pnpm add @eznix/try
npm install @eznix/try
Enter fullscreen mode Exit fullscreen mode

Wrapping Synchronous/Asynchronous Operations

The "Try" library provides functions for both asynchronous and synchronous operations. Here's how you can use it:

import { trySync, tryAsync } from "@eznix/try";

// `fetchUser` is an async function
const tryFetchUserAsync = await tryAsync(fetchUser);
// `fetchUser` is a sync function
const tryFetchUserSync = trySync(fetchUser);
Enter fullscreen mode Exit fullscreen mode

Handling Results with "Try"

The "Try" library offers various methods to handle results effectively:

Access Successful Value

const user = await tryFetchUserAsync.getOrElse({"id": 1, "name": "jimmy"});
Enter fullscreen mode Exit fullscreen mode

Inspect Error

const error = await tryFetchUserAsync.error();
Enter fullscreen mode Exit fullscreen mode

Recover from Failure

const recoveredTry = await tryFetchUserAsync.recover((error) => defaultUser);
Enter fullscreen mode Exit fullscreen mode

Unwrap Result (Carefully)

const result = await tryFetchUserAsync.result();
console.log(result.isOk()); // true
console.log(result.unwrap());
Enter fullscreen mode Exit fullscreen mode

Examples of Using "TryAsync"

Basic Example:

// Wrapping a potentially failing asynchronous operation
const fetchUser = async (id: number): Promise<User> => {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user;
};

const tryFetchUser = await tryAsync(fetchUser(123));

// Handling the result:
const user = await tryFetchUser.getOrElse({"id": 1, "name": "Jimmy"});
console.log("User: ", user.name);
// User: Jimmy
Enter fullscreen mode Exit fullscreen mode

Usage

Plain JavaScript

// Chaining tryAsync to avoid callback nesting

const getUser = async (id) => {
  // API call to fetch user 
};

const getFriends = async (user) => {
  // API call to get user's friends
};

const renderProfile = async (user, friends) => {
  // Render profile page
};

// Without tryAsync
getUser(1)
  .then(user => {
    return getFriends(user) 
      .then(friends => {
        renderProfile(user, friends);
      })
  })
  .catch(err => {
    // Handle error
  });

// With tryAsync
const user = await tryAsync(getUser(1))
  .recover(handleGetUserError)
  .getOrElse({id: 1}); 

const friends = await tryAsync(getFriends(user))
  .recover(handleGetFriendsError)
  .getOrElse([]);

renderProfile(user, friends);
Enter fullscreen mode Exit fullscreen mode

React Example:

import React, { useState, useEffect } from 'react';
import { tryAsync } from '@eznix/try';

function MyComponent() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    async function fetchUser() {
      const user = await tryAsync(fetch('/api/user'))
        .recover(handleFetchError)
        .getOrElse(null);

      setUser(user);
    }

    fetchUser();
  }, []);

  if (!user) {
    return <p>Loading...</p>;
  }

  return <Profile user={user} />;
}

function handleFetchError(err) {
  console.error(err);
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

"Try" brings the clarity of explicit error handling to JavaScript, inspired by the patterns seen in Rust and Golang. By incorporating it into your projects, you enhance code reliability and make error management more straightforward.

Contributing

The project is open for usage. Feel free to contribute and enhance it.

Quick tip

You can wrap any function with trySync or tryAsync. Making your code safer. In case you are not sure if you should handle an error or now.

You can also simplify with:

import { trySync as t, tryAsync as at} from "@eznix/try";
const {errors, data} = at(fetchUser).toPromise()

// but trySync and tryAsync is self explicit. Unfortunately
// we cannot use `try` because it is a reserved word in JS :(

Enter fullscreen mode Exit fullscreen mode

Try it today or check it out on github.

Available on Github

bun add @eznix/try
yarn install @eznix/try
pnpm add @eznix/try
npm install @eznix/try
Enter fullscreen mode Exit fullscreen mode

Happy coding!

Top comments (0)