Skip to content

Error Handling

A functional approach to error handling where operations are like railway tracks - success track and failure track.

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 };
}
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));
}
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 validations
function 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
});
}

Convert exception-throwing code into Result types.

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 version
async 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)));
}
}
// JSON parsing with error handling
function parseJSON<T>(json: string): Result<T, Error> {
return tryCatch(() => JSON.parse(json));
}
// Usage
const result = parseJSON<User>('{"name":"John","age":30}');
if (result.success) {
console.log('User:', result.value);
} else {
console.error('Parse error:', result.error.message);
}
// Async example
async function fetchUser(id: number): Promise<Result<User, Error>> {
return tryCatchAsync(async () => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
}
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);
}
// Usage
const 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);
}
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);
}
// Usage
const 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']
}
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();
}
// Usage
const email = getOrElse(
validateEmail('invalid'),
'default@example.com'
);
// Try alternative source
const userData = orElse(
fetchFromCache(userId),
() => fetchFromAPI(userId)
);
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);
}
// Usage
const result = await retry(
() => fetchUser(123),
3,
1000
);

Collect all validation errors instead of stopping at first error.

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]);
}
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 example
interface 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;
}
// Usage
const 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']
}
function mapError<T, E, F>(
result: Result<T, E>,
fn: (error: E) => F
): Result<T, F> {
return result.success ? result : fail(fn(result.error));
}
// Usage
type 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);
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
}));
}
// Usage
const result = addContext(
validateEmail(email),
{ userId: 123, action: 'signup' }
);

✅ 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

❌ 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