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);
}- π― 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, andokFulfilledfor common patterns - π§© Composable: Easy to integrate into existing codebases
npm install trylessyarn add trylesspnpm add trylessimport { 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'
}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 }
}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);
}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', ... } }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
}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
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');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)));Chain operations that might fail:
const result = await getUser(id)
.andThen(user => getUserPreferences(user.id))
.andThen(prefs => validatePreferences(prefs));Recover from errors:
const result = await getUser(id)
.orElse(error => {
if (error === 'not-found') {
return createDefaultUser();
}
return err(error);
});Get data or provide a default:
const user = getUserById(id).unwrapOr({ id: 0, name: 'Guest' });Compute a fallback from the error:
const value = getPrice(item).unwrapOrElse(error => {
logError(error);
return 0;
});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;
}
}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);
}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);
}
}Creates a success result.
ok() // { success: true, data: undefined }
ok(42) // { success: true, data: 42 }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 }Wraps functions/promises to return Results.
resultfy(dangerousFunction)
resultfy(promise, 'custom-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')Transforms data and wraps in Ok.
okFulfilled((x: number) => x * 2)Returns data or throws UnwrapError.
Returns data or default value.
Returns data or computed default.
Chains operations on success.
Recovers from errors.
Transforms the result.
Type guards for success/error.
We love contributions! Here's how you can help:
- Check if it's already reported in Issues
- If not, create a new issue with:
- Clear description
- Steps to reproduce
- Expected vs actual behavior
- Code example
- Open an issue to discuss it first
- We'll review and provide feedback
- If approved, feel free to submit a PR
- Fork the repository
- Create a new branch:
git checkout -b feature/my-feature - Make your changes
- Run tests:
pnpm --filter tryless test - Run linter:
pnpm --filter tryless lint - Commit:
git commit -m "feat: add my feature" - Push:
git push origin feature/my-feature - Open a Pull Request
# 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- 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
MIT Β© License
If you find Tryless helpful, please consider:
- β Starring the repo on GitHub
- π¦ Sharing it on Twitter
- π Writing about it on your blog