Error Handling
Railway-Oriented Programming
Section titled “Railway-Oriented Programming”A functional approach to error handling where operations are like railway tracks - success track and failure track.
Result Type
Section titled “Result Type”type Result<T, E = Error> = | { success: true; value: T } | { success: false; error: E };
function succeed<T>(value: T): Result<T> { return { success: true, value };}
function fail<E>(error: E): Result<never, E> { return { success: false, error };}Basic Operations
Section titled “Basic Operations”function bind<T, U, E>( result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E> { return result.success ? fn(result.value) : result;}
function map<T, U, E>( result: Result<T, E>, fn: (value: T) => U): Result<U, E> { return result.success ? succeed(fn(result.value)) : result;}
function mapError<T, E, F>( result: Result<T, E>, fn: (error: E) => F): Result<T, F> { return result.success ? result : fail(fn(result.error));}Validation Example
Section titled “Validation Example”function validateEmail(email: string): Result<string, string> { return email.includes('@') ? succeed(email) : fail('Invalid email format');}
function validateLength(value: string, min: number): Result<string, string> { return value.length >= min ? succeed(value) : fail(`Minimum length is ${min}`);}
function validatePassword(password: string): Result<string, string> { const result = validateLength(password, 8); if (!result.success) return result;
return /[A-Z]/.test(password) ? succeed(password) : fail('Password must contain uppercase letter');}
// Chain validationsfunction validateUser(email: string, password: string): Result<{ email: string; password: string }, string> { const emailResult = validateEmail(email); if (!emailResult.success) return emailResult;
const passwordResult = validatePassword(password); if (!passwordResult.success) return passwordResult;
return succeed({ email: emailResult.value, password: passwordResult.value });}Try-Catch Wrapping
Section titled “Try-Catch Wrapping”Convert exception-throwing code into Result types.
tryCatch Function
Section titled “tryCatch Function”function tryCatch<T>( fn: () => T): Result<T, Error> { try { return succeed(fn()); } catch (error) { return fail(error instanceof Error ? error : new Error(String(error))); }}
// Async versionasync function tryCatchAsync<T>( fn: () => Promise<T>): Promise<Result<T, Error>> { try { const value = await fn(); return succeed(value); } catch (error) { return fail(error instanceof Error ? error : new Error(String(error))); }}Usage Example
Section titled “Usage Example”// JSON parsing with error handlingfunction parseJSON<T>(json: string): Result<T, Error> { return tryCatch(() => JSON.parse(json));}
// Usageconst result = parseJSON<User>('{"name":"John","age":30}');
if (result.success) { console.log('User:', result.value);} else { console.error('Parse error:', result.error.message);}
// Async exampleasync function fetchUser(id: number): Promise<Result<User, Error>> { return tryCatchAsync(async () => { const response = await fetch(`/api/users/${id}`); return response.json(); });}Combining Results
Section titled “Combining Results”Sequence - All or Nothing
Section titled “Sequence - All or Nothing”function sequence<T, E>(results: Result<T, E>[]): Result<T[], E> { const values: T[] = [];
for (const result of results) { if (!result.success) { return result as Result<T[], E>; } values.push(result.value); }
return succeed(values);}
// Usageconst results = [ validateEmail('user@example.com'), validateEmail('another@example.com'), validateEmail('third@example.com')];
const combined = sequence(results);
if (combined.success) { console.log('All emails valid:', combined.value);} else { console.error('Validation failed:', combined.error);}Collect - Gather All Errors
Section titled “Collect - Gather All Errors”function collect<T, E>(results: Result<T, E>[]): Result<T[], E[]> { const values: T[] = []; const errors: E[] = [];
for (const result of results) { if (result.success) { values.push(result.value); } else { errors.push(result.error); } }
return errors.length > 0 ? fail(errors) : succeed(values);}
// Usageconst validations = [ validateEmail('user@example.com'), validateEmail('invalid-email'), validateEmail('another@example.com'), validateEmail('also-invalid')];
const collected = collect(validations);
if (collected.success) { console.log('All valid:', collected.value);} else { console.error('Errors:', collected.error); // ['Invalid email format', 'Invalid email format']}Error Recovery
Section titled “Error Recovery”Providing Defaults
Section titled “Providing Defaults”function getOrElse<T, E>(result: Result<T, E>, defaultValue: T): T { return result.success ? result.value : defaultValue;}
function orElse<T, E>( result: Result<T, E>, alternative: () => Result<T, E>): Result<T, E> { return result.success ? result : alternative();}
// Usageconst email = getOrElse( validateEmail('invalid'), 'default@example.com');
// Try alternative sourceconst userData = orElse( fetchFromCache(userId), () => fetchFromAPI(userId));Retrying Operations
Section titled “Retrying Operations”async function retry<T, E>( fn: () => Promise<Result<T, E>>, maxAttempts: number, delay: number = 1000): Promise<Result<T, E>> { let attempt = 1;
while (attempt <= maxAttempts) { const result = await fn();
if (result.success) { return result; }
if (attempt < maxAttempts) { await new Promise(resolve => setTimeout(resolve, delay)); }
attempt++; }
return fail('Max retry attempts exceeded' as E);}
// Usageconst result = await retry( () => fetchUser(123), 3, 1000);Validation Accumulation
Section titled “Validation Accumulation”Collect all validation errors instead of stopping at first error.
Validation Applicative
Section titled “Validation Applicative”interface Validation<E, T> { isValid: boolean; value?: T; errors: E[];}
function valid<T>(value: T): Validation<never, T> { return { isValid: true, value, errors: [] };}
function invalid<E>(errors: E[]): Validation<E, never> { return { isValid: false, errors };}
function validate<T, E>( predicate: (value: T) => boolean, error: E, value: T): Validation<E, T> { return predicate(value) ? valid(value) : invalid([error]);}Combining Validations
Section titled “Combining Validations”function combine<A, B, C, E>( va: Validation<E, A>, vb: Validation<E, B>, fn: (a: A, b: B) => C): Validation<E, C> { if (va.isValid && vb.isValid) { return valid(fn(va.value!, vb.value!)); }
return invalid([...va.errors, ...vb.errors]);}
// Usage exampleinterface UserForm { email: string; password: string; age: number;}
function validateUserForm(form: UserForm): Validation<string, UserForm> { const emailValidation = validate( (email: string) => email.includes('@'), 'Invalid email', form.email );
const passwordValidation = validate( (password: string) => password.length >= 8, 'Password too short', form.password );
const ageValidation = validate( (age: number) => age >= 18, 'Must be 18 or older', form.age );
// Combine all validations const combined = combine( emailValidation, combine(passwordValidation, ageValidation, (password, age) => ({ password, age })), (email, rest) => ({ email: email, ...rest }) );
return combined;}
// Usageconst validation = validateUserForm({ email: 'invalid-email', password: 'short', age: 15});
if (!validation.isValid) { console.error('Validation errors:', validation.errors); // ['Invalid email', 'Password too short', 'Must be 18 or older']}Error Transformation
Section titled “Error Transformation”Mapping Error Types
Section titled “Mapping Error Types”function mapError<T, E, F>( result: Result<T, E>, fn: (error: E) => F): Result<T, F> { return result.success ? result : fail(fn(result.error));}
// Usagetype ValidationError = { field: string; message: string };type HttpError = { status: number; message: string };
function toHttpError(validationError: ValidationError): HttpError { return { status: 400, message: `${validationError.field}: ${validationError.message}` };}
const validationResult: Result<User, ValidationError> = validateEmail(email);const httpResult: Result<User, HttpError> = mapError(validationResult, toHttpError);Error Context
Section titled “Error Context”interface ErrorContext<E> { error: E; timestamp: Date; context: Record<string, any>;}
function addContext<T, E>( result: Result<T, E>, context: Record<string, any>): Result<T, ErrorContext<E>> { return mapError(result, error => ({ error, timestamp: new Date(), context }));}
// Usageconst result = addContext( validateEmail(email), { userId: 123, action: 'signup' });Best Practices
Section titled “Best Practices”✅ Use Result/Either types for error handling ✅ Accumulate validation errors ✅ Provide meaningful error messages ✅ Transform errors appropriately ✅ Chain operations with bind/flatMap ✅ Handle all error cases explicitly ✅ Use type-safe error types ✅ Add context to errors ✅ Retry transient failures ✅ Provide fallback values
Don’ts
Section titled “Don’ts”❌ Mix exceptions with Result types ❌ Ignore error cases ❌ Lose error information ❌ Use generic error messages ❌ Throw exceptions in pure functions ❌ Forget to handle async errors ❌ Create deeply nested error handling ❌ Use try-catch excessively