Skip to content

eznix86/try

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Try

Bundle size

Concept

This package aims to simulate an approach where errors are treated as values, similar to programming languages like Rust and Golang, where errors are explicit.

Check also Railway Oriented Programming.

In Rust, using the Result Pattern:

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

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

In Golang, errors are implicit:

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

Why?

Errors are not explicit in Javascript. Importing an external library or using your own code can introduce errors that can be difficult to track down. So your code is based on trust alone.

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

await fetchUser(); // should we handle the errors ?
leftpad(); // should we handle the errors ?

Try is a wrapper for your functions. It enforces the need to handle errors and errors is not thrown, they are returned as values. The library also aims to eliminate callback hell (nested then) and the tower of doom (try-catch block).

By treating errors as values, the project simplifies error handling, making it more explicit and reducing the chances of overlooking errors.

Note

We also support synchronous function too. Use trySync, it will return no promise.

Getting Started

bun add @eznix/try
yarn install @eznix/try
pnpm add @eznix/try
npm install @eznix/try

Wrapping Operations

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

// `fetchUser` is an async function
const tryFetchUser = await tryAsync(fetchUser);
// `fetchUser` is an sync function
const tryFetchUser = trySync(fetchUser);

Handling Results

Access Successful Value

const user = await tryFetchUser.getOrElse({"id": 1, "name": "jimmy"}); // Use a default value if the operation failed

Inspect Error

const error = await tryFetchUser.error(); // Get the error if the operation failed

Recover from Failure

// Compared to `getOrElse()`, you can try other actions, like another fetch.
// It will return errors as a value.
const recoveredTry = await tryFetchUser.recover((error) => defaultUser);

Unwrap Result (Carefully)

const result = await tryFetchUser.result();
console.log(result.isOk()); // true
console.log(result.unwrap());

Examples

Examples of using tryAsync.

Basic

// Wrapping a potentially failing asynchronous operation
// Like network failure or a bad response from the server
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

All features

// Example Usage:
(async () => {
    // Trying an asynchronous operation using `tryAsync`
    let failFn = await tryAsync<number, any>(async () => {
        throw new Error('Failed');
    });

    let fn = await tryAsync<{name: string}, any>(async () => {
        return {name: 'John'};
    });

    // Getting the result value or a default if there is an error
    let value: {name: string} = await fn.getOrElse({name: 'Default'});
    console.log('Value:', value); // {name: 'John'}

    // Getting the error, if any
    let error = await failFn.error();
    console.log('Error:', error); // Error: Failed

    // Getting both errors and data in a single Promise
    let { error: errors, data } = await fn.toPromise();
    console.log('Errors:', errors); // null
    console.log('Data:', data); // {name: 'John'}

    // Recovering from errors by providing a fallback value
    let recoveredTry = await failFn.recover((error) => 
    // perform another network call
    {name: "Recover"}
    );
    console.log('RecoveredTry:', recoveredTry); // Promise<Try<number, {name: string}>>

    let { data: recovered } = await recoveredTry.toPromise();
    console.log('Recovered:', recovered); // 100

    // Getting the result in a structured way
    let resultPattern = await fn.result();

    // Handling the result using the Result class
    if (resultPattern.isOk() && !resultPattern.isNull()) {
        console.log("ResultPattern", resultPattern.unwrap()); // {name: 'John'}
    } else {
        console.log("ResultPattern", resultPattern.unwrapErr()); // null
    }
})();

Usage with Javascript framework

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

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;
}

Vue

<template>
  <div v-if="loading">
    Loading...
  </div>

  <div v-else-if="error">
    {{ errorMessage }}
  </div>

  <div v-else>
    <UserProfile :user="user"/>
  </div>
</template>

<script>
import { ref } from 'vue';
import { tryAsync } from '@eznix/try';

export default {
  setup() {
    const user = ref(null);
    const loading = ref(true);
    const error = ref(null);

    async function fetchUser() {
      user.value = await tryAsync(loadUser)
        .recover(handleError)
        .getOrElse(null);
      
      loading.value = false;
    }

    function handleError(err) {
      error.value = err;
    }

    function loadUser() {
      return fetch('/api/user').then(res => res.json());
    }

    fetchUser();

    return { user, loading, error };
  }
}
</script>

Svelte

<script>
import { tryAsync } from '@eznix/try';

let user = null;
let error = null;
let loading = true;

async function getUser() {
  user = await tryAsync(fetchUser)
    .recover(handleError)
    .getOrElse(null);
  
  loading = false; 
}

function handleError(err) {
  error = err;
}

async function fetchUser() {
  // API call to fetch user
}

getUser();
</script>

{#if loading}
  <p>Loading...</p>
{:else if error}
  <p>Error: {error.message}</p>  
{:else}
  <UserProfile {user} />
{/if}

Contributing

Feel free to open an issue or a pull request. Please make sure to run bun test before opening a pull request.

TODO

  • Create a unified function to wrap sync functions and async functions and returns the appropriate type/signature.

About

Treat async errors as values

Resources

Stars

Watchers

Forks

Packages

No packages published