Skip to content

jordyfontoura/TryLess

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ Tryless

Type-safe error handling for TypeScript without try-catch hell

npm version CI License TypeScript npm downloads


🎯 Why Tryless?

Say goodbye to deeply nested try-catch blocks and hello to elegant, type-safe error handling. Tryless brings Rust-inspired Result types to TypeScript with a developer experience that just feels right.

Before (nested try-catch hell):

async function fetchUser(id: string) {
  try {
    const response = await fetch(`/api/users/${id}`);
    try {
      const json = await response.json();
      try {
        const user = userSchema.parse(json);
        return user;
      } catch (error) {
        throw new Error('Invalid user schema', { cause: error });
      }
    } catch (error) {
      throw new Error('Invalid JSON', { cause: error });
    }
  } catch (error) {
    throw new Error('Fetch failed', { cause: error });
  }
}

After (clean & type-safe):

import { ok, err, resultfy } from 'tryless';

async function fetchUser(id: string) {
  const responseResult = await resultfy(
    fetch(`/api/users/${id}`),
    'fetch-failed'
  );
  if (!responseResult.success) return responseResult;

  const jsonResult = await resultfy(
    responseResult.data.json(),
    'invalid-json'
  );
  if (!jsonResult.success) return jsonResult;

  const userResult = userSchema.safeParse(jsonResult.data);
  if (!userResult.success) return err('invalid-schema', userResult.error);

  return ok(userResult.data);
}

✨ Features

  • 🎯 Type-Safe: Full TypeScript support with discriminated unions
  • πŸ”— Chainable: Elegant API with andThen, orElse, and more
  • 🎨 Zero Dependencies: Lightweight and fast
  • πŸ” Rich Stack Traces: Detailed error tracking for debugging
  • 🌊 Promise-Ready: Built for async/await workflows
  • πŸ› οΈ Practical Helpers: resultfy, errReject, and okFulfilled for common patterns
  • 🧩 Composable: Easy to integrate into existing codebases

πŸ“¦ Installation

npm install tryless
yarn add tryless
pnpm add tryless

πŸš€ Quick Start

Basic Usage

import { ok, err } from 'tryless';

function divide(a: number, b: number) {
  if (b === 0) return err('division-by-zero');
  
  return ok(a / b);
}

const result = divide(10, 2);

if (result.success) {
  console.log(result.data); // 5
} else {
  console.log(result.error); // 'division-by-zero'
}

With Promises

import { resultfy } from 'tryless';

async function getUser(id: string) {
  // Convert promise rejection to error result
  const result = await resultfy(
    fetch(`/api/users/${id}`),
    'user-fetch-failed'
  );
    
  if (!result.success) {
    return result; // { success: false, error: 'user-fetch-failed', reason: ... }
  }
  
  return result; // { success: true, data: Response }
}

Early Returns Pattern

Tryless promotes clean code with early returns:

import { ok, err } from 'tryless';

async function validateAndCreateUser(data: unknown) {
  // Validate email
  if (!data.email) return err('missing-email');
  
  // Check if user exists
  const existingUser = await findUserByEmail(data.email);
  if (existingUser.success) return err('user-already-exists');
  
  // Validate age
  if (data.age < 18) return err('user-underage');
  
  // Create user
  const user = await createUser(data);
  return ok(user);
}

πŸŽ“ Core Concepts

Ok and Err

The foundation of Tryless: two simple types that represent success and failure.

import { ok, err } from 'tryless';

// Success result
const success = ok({ id: 1, name: 'John' });
// { success: true, data: { id: 1, name: 'John' } }

// Error result with message
const failure = err('not-found');
// { success: false, error: 'not-found', reason: undefined }

// Error with additional context
const detailedError = err('validation-failed', { field: 'email', message: 'Invalid format' });
// { success: false, error: 'validation-failed', reason: { field: 'email', ... } }

Checking Results

Use the success property to safely narrow types:

const result = divide(10, 2);

if (result.success) {
  // TypeScript knows this is Ok<number>
  console.log(result.data); // βœ… Type-safe access
} else {
  // TypeScript knows this is Err
  console.log(result.error); // βœ… Type-safe access
  console.log(result.reason); // βœ… Type-safe access
}

πŸ› οΈ Powerful Helpers

resultfy

Recommended approach for wrapping promises and functions to always return Results instead of throwing:

import { resultfy } from 'tryless';

// Wrap a promise with custom error message (most common use case)
const userResult = await resultfy(
  fetch('/api/user').then(r => r.json()),
  'fetch-error'
);

if (!userResult.success) {
  console.log(userResult.error); // 'fetch-error'
  console.log(userResult.reason); // Original error details
}

// Wrap a function
const safeDivide = resultfy((a: number, b: number) => {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
});

const result = safeDivide(10, 2);
// { success: true, data: 5 }

const errorResult = safeDivide(10, 0);
// { success: false, error: 'unknown', reason: Error('Division by zero') }

Why prefer resultfy over .then(ok, errReject())?

  • βœ… More concise and readable
  • βœ… Works with any promise or function
  • βœ… Custom error messages built-in
  • βœ… Consistent API

errReject

Perfect for promise chains - converts rejections to error results:

import { ok, errReject, resultfy } from 'tryless';

// Still useful for complex promise chains
const result = await fetch('/api/data')
  .then(ok, errReject('network-error'))
  .then(res => res.success ? res.data.json() : res)
  .then(ok, errReject('parse-error'));

// Or use resultfy for simpler cases
const result = await resultfy(fetch('/api/data'), 'network-error');

okFulfilled

Transform data and wrap in a success result - great for mapping:

import { okFulfilled } from 'tryless';

const double = okFulfilled((n: number) => n * 2);
const result = double(5);
// { success: true, data: 10 }

// In promise chains
const users = await fetch('/api/users')
  .then(r => r.json())
  .then(okFulfilled(data => data.users))
  .then(okFulfilled(users => users.map(u => u.name)));

⚑ Chaining Operations

andThen

Chain operations that might fail:

const result = await getUser(id)
  .andThen(user => getUserPreferences(user.id))
  .andThen(prefs => validatePreferences(prefs));

orElse

Recover from errors:

const result = await getUser(id)
  .orElse(error => {
    if (error === 'not-found') {
      return createDefaultUser();
    }
    return err(error);
  });

unwrapOr

Get data or provide a default:

const user = getUserById(id).unwrapOr({ id: 0, name: 'Guest' });

unwrapOrElse

Compute a fallback from the error:

const value = getPrice(item).unwrapOrElse(error => {
  logError(error);
  return 0;
});

🎯 Real-World Examples

API Request with Validation

import { ok, err, resultfy } from 'tryless';
import { z } from 'zod';

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

async function fetchAndValidateUser(id: string) {
  // Fetch data
  const fetchResult = await resultfy(
    fetch(`/api/users/${id}`),
    'network-error'
  );
  if (!fetchResult.success) return fetchResult;

  // Check response status
  const response = fetchResult.data;
  if (!response.ok) {
    return err('http-error', { status: response.status });
  }

  // Parse JSON
  const jsonResult = await resultfy(
    response.json(),
    'json-parse-error'
  );
  if (!jsonResult.success) return jsonResult;

  // Validate schema
  const validation = userSchema.safeParse(jsonResult.data);
  if (!validation.success) {
    return err('validation-error', validation.error);
  }

  return ok(validation.data);
}

// Usage
const userResult = await fetchAndValidateUser('123');

if (userResult.success) {
  console.log('User:', userResult.data);
} else {
  switch (userResult.error) {
    case 'network-error':
      console.error('Network failed:', userResult.reason);
      break;
    case 'http-error':
      console.error('HTTP error:', userResult.reason);
      break;
    case 'validation-error':
      console.error('Invalid data:', userResult.reason);
      break;
  }
}

Database Operations

import { ok, err, resultfy } from 'tryless';

async function createUser(data: UserInput) {
  // Validate input
  if (!data.email) return err('missing-email');
  if (!data.name) return err('missing-name');

  // Check if exists
  const existing = await db.findByEmail(data.email);
  if (existing) return err('email-already-exists', { email: data.email });

  // Create user (wrapped to catch DB errors)
  const createResult = await resultfy(
    db.users.insert(data),
    'database-error'
  );

  if (!createResult.success) return createResult;

  return ok(createResult.data);
}

File Operations

import { readFile } from 'fs/promises';
import { ok, err, resultfy } from 'tryless';

async function loadConfig(path: string) {
  const fileResult = await resultfy(
    readFile(path, 'utf-8'),
    'file-read-error'
  );
  if (!fileResult.success) return fileResult;

  try {
    const config = JSON.parse(fileResult.data);
    return ok(config);
  } catch (error) {
    return err('json-parse-error', error);
  }
}

πŸ“š API Reference

Core Functions

ok(data?)

Creates a success result.

ok() // { success: true, data: undefined }
ok(42) // { success: true, data: 42 }

err(error?, reason?)

Creates an error result.

err() // { success: false, error: 'unknown', reason: undefined }
err('not-found') // { success: false, error: 'not-found', reason: undefined }
err('validation-failed', details) // { success: false, error: 'validation-failed', reason: details }

Helper Functions

resultfy(fn, error?)

Wraps functions/promises to return Results.

resultfy(dangerousFunction)
resultfy(promise, 'custom-error')

errReject(error)

Converts promise rejections to Err results. Useful for complex promise chains.

promise.then(ok, errReject('fetch-failed'))

// For simpler cases, prefer resultfy:
resultfy(promise, 'fetch-failed')

okFulfilled(mapFn)

Transforms data and wraps in Ok.

okFulfilled((x: number) => x * 2)

Result Methods

.unwrap(customError?)

Returns data or throws UnwrapError.

.unwrapOr(defaultValue)

Returns data or default value.

.unwrapOrElse(fn)

Returns data or computed default.

.andThen(fn)

Chains operations on success.

.orElse(fn)

Recovers from errors.

.map(fn)

Transforms the result.

.isOk() / .isErr()

Type guards for success/error.

🀝 Contributing

We love contributions! Here's how you can help:

πŸ› Found a Bug?

  1. Check if it's already reported in Issues
  2. If not, create a new issue with:
    • Clear description
    • Steps to reproduce
    • Expected vs actual behavior
    • Code example

πŸ’‘ Have a Feature Idea?

  1. Open an issue to discuss it first
  2. We'll review and provide feedback
  3. If approved, feel free to submit a PR

πŸ”§ Want to Contribute Code?

  1. Fork the repository
  2. Create a new branch: git checkout -b feature/my-feature
  3. Make your changes
  4. Run tests: pnpm --filter tryless test
  5. Run linter: pnpm --filter tryless lint
  6. Commit: git commit -m "feat: add my feature"
  7. Push: git push origin feature/my-feature
  8. Open a Pull Request

πŸ“ Development Setup

# Clone the repository
git clone https://github.com/jordyfontoura/tryless.git
cd tryless

# Install dependencies
pnpm install

# Run tests
pnpm --filter tryless test

# Run tests in watch mode
pnpm --filter tryless test -- --watch

# Build the package
pnpm --filter tryless build

# Run linter
pnpm --filter tryless lint

βœ… Pull Request Guidelines

  • Write clear, descriptive commit messages
  • Follow existing code style
  • Add tests for new features
  • Update documentation if needed
  • Keep PRs focused on a single concern
  • All tests must pass

πŸ“„ License

MIT Β© License

🌟 Show Your Support

If you find Tryless helpful, please consider:

  • ⭐ Starring the repo on GitHub
  • 🐦 Sharing it on Twitter
  • πŸ“ Writing about it on your blog

Made with ❀️ by developers who hate try-catch hell

GitHub β€’ npm β€’ Issues