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?
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,
};
}
Error Handling in Golang:
f, err := os.Open("filename.ext")
if err != nil {
// handle errors
}
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
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);
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"});
Inspect Error
const error = await tryFetchUserAsync.error();
Recover from Failure
const recoveredTry = await tryFetchUserAsync.recover((error) => defaultUser);
Unwrap Result (Carefully)
const result = await tryFetchUserAsync.result();
console.log(result.isOk()); // true
console.log(result.unwrap());
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
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);
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;
}
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 :(
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
Happy coding!
Top comments (0)