Hey there, fellow coder! 👋 Tired of wrestling with unpredictable outcomes in your TypeScript projects? Say hello to Fates, your new best friend in the battle against uncertainty!
- Installation
- Getting Started
- Core Concepts
- Available Crates
- Advanced Usage
- Best Practices
- Performance Considerations
- Migration Guide
- Library Comparison
- API Reference
npm install fates
# or
yarn add fates
# or
pnpm add fatesFates makes error handling intuitive and type-safe:
import{ok,err,typeResult}from'fates';// Results use Error as the default error typefunctiondivide(a: number,b: number): Result<number>{if(b===0)returnerr(newError("Division by zero"));returnok(a/b);}// Pattern matching for clean error handlingdivide(10,2).match({ok: result=>console.log(`Result: ${result}`),err: error=>console.log(`Error: ${error.message}`)});Chain operations safely and handle errors gracefully:
constresult=divide(10,2).map(value=>value*2).flatMap(value=>validateNumber(value)).mapErr(error=>newValidationError(error.message));// Provide fallback valuesconstsafeResult=result.unwrapOr(0);// Transform errorsconsthandled=result.mapErr(error=>{logError(error);returnnewUserFacingError("Calculation failed");});Working with boolean Results requires special attention:
functioncheckUserAccess(userId: string): Result<boolean>{try{consthasAccess=/* check access */;returnok(hasAccess);}catch(error){returnerr(errorinstanceofError ? error : newError(String(error)));}}// ✅ Correct usage with pattern matchingconstaccessResult=checkUserAccess("user-123");accessResult.match({ok: hasAccess=>hasAccess ? grantAccess() : denyAccess(),err: error=>handleError(error)});// ✅ Alternative using mapconstaccessStatus=awaitcheckUserAccess("user-123").map(hasAccess=>hasAccess ? "granted" : "denied").unwrapOr("error");Result<T, E = Error> represents an operation that can fail:
// Error type defaults to Error if not specifiedfunctionfindUser(id: string): Result<User>{try{constuser=db.find(id);returnuser ? ok(user) : err(newError("User not found"));}catch(error){returnerr(errorinstanceofError ? error : newError(String(error)));}}// Custom error typefunctionvalidateAge(age: number): Result<number,ValidationError>{returnage>=0 ? ok(age) : err(newValidationError("Age must be positive"));}Option<T> handles nullable values elegantly:
import{some,none,typeOption}from'fates';functionfindFirst<T>(arr: T[],predicate: (value: T)=>boolean): Option<T>{constvalue=arr.find(predicate);returnvalue ? some(value) : none();}// Chain operations safelyconstresult=findFirst([1,2,3],x=>x>2).map(x=>x*2).filter(x=>x<10).unwrapOr(0);Either<L, R> represents values with two possible types:
import{left,right,typeEither}from'fates';typeValidationErrors=string[];typeConfigData={port: number;host: string};functionparseConfig(input: string): Either<ValidationErrors,ConfigData>{consterrors=validateConfig(input);returnerrors.length>0 ? left(errors)
: right(parseValidConfig(input));}// Usage with pattern matchingparseConfig(configString).match({left: errors=>console.error("Validation failed:",errors),right: config=>startServer(config)});Fates provides specialized modules for common tasks. Each crate is independently importable for optimal tree-shaking:
- Assert - Type-safe assertions and runtime checks
- Cache - Simple, flexible caching with TTL support
- Error - Enhanced error types with metadata
- Events - Type-safe event handling
- Fetch - HTTP client with Result returns
- FileSystem - Safe filesystem operations
- Path - Path manipulation utilities
- Rate Limiter - Request rate control
- React - React hooks and components
Example using multiple crates:
import{Http}from'fates/fetch';import{RateLimiter}from'fates/rate-limiter';import{assertDefined}from'fates/assert';constapi=newHttp('https://api.example.com');constlimiter=newRateLimiter({interval: 1000,maxRequests: 10});asyncfunctionfetchUser(id: string){assertDefined(id,"User ID must be defined");if(!limiter.tryAcquire()){returnerr(newError("Rate limit exceeded"));}returnawaitapi.get<User>(`/users/${id}`);}Handle asynchronous operations elegantly:
import{typeAsyncResult,tryAsync}from'fates';asyncfunctionprocessUserData(id: string): AsyncResult<ProcessedData>{returntryAsync(async()=>{constuser=awaitfetchUser(id);constvalidated=awaitvalidateUser(user);returnawaitprocessData(validated);});}// Chain async operationsconstresult=awaitprocessUserData("123").then(result=>result.map(enrichData).mapErr(error=>newProcessingError(error)));Build complex operations from simple ones:
constvalidateUser=(user: User): Result<User>=>validateName(user).flatMap(validateAge).flatMap(validateEmail).map(enrichUserData);// Compose functions that return ResultsconstprocessUser=chain(validateUser,updateDatabase,notifyUser);Implement sophisticated error handling:
constresult=awaitfetchUser(id).then(result=>result.orElse(error=>{if(errorinstanceofNotFoundError){returnfetchUserFromBackup(id);}returnerr(error);}).mapErr(error=>{logError(error);returnnewUserFacingError("Could not fetch user");}));Create reusable validation chains:
import{Validation,valid,invalid}from'fates/validation';constvalidateUsername=(input: string): Validation<string>=>{if(input.length<3)returninvalid("Too short");if(input.length>20)returninvalid("Too long");if(!/^[a-zA-Z0-9_]+$/.test(input))returninvalid("Invalid characters");returnvalid(input);};constvalidateUser=(user: unknown): Validation<User>=>validateUsername(user.username).flatMap(username=>validateEmail(user.email).map(email=>({ username, email })));Build robust data processing pipelines:
import{pipeline}from'fates/utils';constprocessOrder=pipeline(validateOrder,enrichWithUserData,calculateTotals,applyDiscounts,saveToDatabase,notifyCustomer);constresult=awaitprocessOrder(orderData);Handle transient failures:
import{retry}from'fates/utils';constfetchWithRetry=retry(()=>api.get('/unstable-endpoint'),{maxAttempts: 3,delay: 1000,backoff: 2});constresult=awaitfetchWithRetry();Work with multiple Results:
import{all,any}from'fates/utils';// Wait for all operations to succeedconstresults=awaitall([fetchUser(id),fetchOrders(id),fetchPreferences(id)]);// Use first successful resultconstbackup=awaitany([primaryDB.fetch(id),secondaryDB.fetch(id),tertiaryDB.fetch(id)]);// Use pattern matching for exhaustive handlingresult.match({ok: value=>handleSuccess(value),err: error=>handleError(error)});// Chain operations safelyoption.map(transform).flatMap(validate);// Handle errors explicitlyresult.mapErr(error=>newApplicationError(error));// Use type guardsif(result.isOk()){// TypeScript knows result is Ok<T>}// Don't access .value directlyresult.value// ❌ Never do this!// Don't use unwrap without protectionresult.unwrap()// ❌ Could throw!// Don't ignore error casesresult.map(value=>transform(value))// ❌ Error case ignored// Don't mix with null/undefinedfunctionfindUser(): User|null// ❌ Use Option<User>- Results and Options are lightweight wrappers with minimal overhead
- Method chaining creates new instances; batch operations when possible
- Use
matchfor pattern matching - it's optimized and type-safe - Async operations leverage native Promises for optimal performance
- Tree-shaking friendly - only pay for what you use
- Crates are independently importable for minimal bundle size
Before:
try{constuser=awaitfetchUser(id);constvalidated=validateUser(user);returnprocessUser(validated);}catch(error){handleError(error);returndefaultUser;}After:
constresult=awaitfetchUser(id).then(result=>result.flatMap(validateUser).flatMap(processUser).unwrapOr(defaultUser));Before:
functionfindUser(id: string): User|null{constuser=users.get(id);returnuser??null;}After:
functionfindUser(id: string): Option<User>{constuser=users.get(id);returnuser ? some(user) : none();}- fp-ts: Complete FP toolkit, steeper learning curve
- neverthrow: Similar approach, fewer features
- ts-results: Basic Result type only
- Option-T: Focused on Option type
- Fates:
- Comprehensive but approachable
- Rich utility functions
- Strong TypeScript integration
- Modular architecture
- First-class async support
- React integration
- Wide range of utility crates
For detailed API documentation, see API.md.
ISC License - see LICENSE for details.
Ready to tame uncertainty in your TypeScript projects? Get started with Fates today!
npm install fates