@deessejs/fp

Result

Explicit success and failure with typed errors

The Result type represents a value that can be either a success (Ok) or a failure (Err). It's the foundation of explicit error handling in @deessejs/fp.

Why Result?

TypeScript promises type safety, but traditional error handling with exceptions breaks that promise. When a function can throw, the type system can't help you handle errors, they're invisible until runtime.

The Problem with Exceptions

// This function CAN throw, but the type says it returns User
function processUser(data: string): User {
  return JSON.parse(data); // No error in the type!
}

// Callers have no idea this can fail
try {
  const user = processUser(data);
} catch (e) {
  // But what if you forget?
}

Problems:

  • Errors are invisible in the type signature
  • TypeScript can't enforce error handling at compile time
  • Easy to forget try/catch, leading to unhandled exceptions

The Result Solution

With Result, errors become explicit in the type. The err() function wraps errors, and the error type E can be anything from simple strings to rich domain objects.

For structured domain errors with enrichment capabilities (notes, cause chaining, Zod validation), see the Error documentation.

import { ok, err, isOk, Result, error, ExtractError } from '@deessejs/fp';
import { z } from 'zod';

const ParseError = error({
  name: 'ParseError',
  schema: z.object({ message: z.string() }),
  message: (args) => `Parse failed: ${args.message}`,
});

function parseUser(data: string): Result<User, ExtractError<typeof ParseError>> {
  try {
    return ok(JSON.parse(data));
  } catch (e) {
    return err(ParseError({ message: e instanceof Error ? e.message : 'Unknown' }));
  }
}

const result = parseUser(data);

if (isOk(result)) {
  console.log(result.value.name);
} else {
  console.error('Failed:', result.error.message);
  // Access enrichment: result.error.notes, result.error.cause, etc.
}

Key concepts:

  • Result<User, Error> - Two generic types: success value (User) and error type (Error)
  • ok(value) - Wraps success: { ok: true, value: T }
  • err(error) - Wraps failure: { ok: false, error: E }
  • isOk() - Type guard that narrows to the success branch
  • result.error - Access the error with its notes and cause chain (see Error)

Core Concepts

Ok and Err

Result<T, E> has two possible states:

StateDescriptionProperties
Ok<T>Successok: true, value: T
Err<E>Failureok: false, error: E
import { ok, err, Result } from '@deessejs/fp';

// Success
const success: Result<number, string> = ok(42);

// Failure
const failure: Result<number, string> = err('Something went wrong');

The Error Type

The error type E is completely flexible. For rich domain errors with enrichment, see the Error documentation.

// String errors
const r1 = err('simple error');

// Structured errors
const r2 = err({ code: 'NOT_FOUND', message: 'User not found' });

// Custom error types
const r3 = err(new Error('something failed'));

Reference

Creating Results

ok(value)

Creates a success result.

import { ok } from '@deessejs/fp';

ok(42);        // Ok(42)
ok('hello');   // Ok('hello')
ok({ id: 1 }); // Ok({ id: 1 })

err(error)

Creates a failure result.

import { err } from '@deessejs/fp';

err('error');                              // Err('error')
err({ code: 404, message: 'Not found' }); // Err({ code: 404, message: 'Not found' })

Type Guards

isOk(result)

Type guard that narrows to the success branch.

import { ok, err, isOk } from '@deessejs/fp';

const result = ok(42);

if (isOk(result)) {
  console.log(result.value); // 42
}

isErr(result)

Type guard that narrows to the failure branch.

import { ok, err, isErr } from '@deessejs/fp';

const result = err('oops');

if (isErr(result)) {
  console.log(result.error); // 'oops'
}

Transformation

map(result, fn)

Transforms the success value. Passes errors through unchanged.

import { ok, err, map } from '@deessejs/fp';

map(ok(2), x => x * 2);        // Ok(4)
map(err('error'), x => x * 2); // Err('error')

Also available as a method:

ok(2).map(x => x * 2); // Ok(4)

flatMap(result, fn)

Chains operations that can fail. If Ok, applies the function. If Err, returns Err.

import { ok, err, flatMap } from '@deessejs/fp';

const findUser = (id: number) =>
  id > 0 ? ok({ id, name: 'John' }) : err('Invalid id');

const getEmail = (user: { name: string }) =>
  ok(user.name.toLowerCase() + '@example.com');

flatMap(ok(1), findUser);           // Ok({ id: 1, name: 'John' })
flatMap(ok(-1), findUser);          // Err('Invalid id')
flatMap(ok(1), x => flatMap(findUser(x.id), getEmail)); // Ok('john@example.com')

Also available as a method:

ok(1).flatMap(findUser); // Ok({ id: 1, name: 'John' })

mapErr(result, fn)

Transforms the error. Passes success through unchanged.

import { ok, err, mapErr } from '@deessejs/fp';

mapErr(err('not found'), e => new Error(e)); // Err(Error: 'not found')
mapErr(ok(42), e => new Error(e));            // Ok(42)

Also available as a method:

err('not found').mapErr(e => new Error(e)); // Err(Error: 'not found')

Extraction

getOrElse(result, defaultValue)

Returns the success value, or a default if Err.

import { ok, err, getOrElse } from '@deessejs/fp';

getOrElse(ok(42), 0);      // 42
getOrElse(err('oops'), 0); // 0

Also available as a method:

ok(42).getOrElse(0);      // 42
err('oops').getOrElse(0); // 0

getOrCompute(result, fn)

Returns the success value, or computes one lazily if Err. Useful for expensive fallbacks.

import { ok, err, getOrCompute } from '@deessejs/fp';

const expensive = () => { console.log('computing...'); return 0; };

getOrCompute(ok(42), expensive); // 42 (never logs)
getOrCompute(err('oops'), expensive); // logs 'computing...', returns 0

Also available as a method:

err('oops').getOrCompute(() => 0); // 0

Side Effects

tap(result, fn)

Executes a function on the value without changing it. Useful for logging.

import { ok, err, tap } from '@deessejs/fp';

tap(ok(42), x => console.log('Got:', x)); // Logs: 'Got: 42', returns Ok(42)
tap(err('oops'), x => console.log('Got:', x)); // Nothing logged, returns Err('oops')

Also available as a method:

ok(42).tap(x => console.log(x)); // Ok(42)

tapErr(result, fn)

Executes a function on the error without changing it.

import { ok, err, tapErr } from '@deessejs/fp';

tapErr(ok(42), e => console.error('Error:', e));     // Nothing logged
tapErr(err('oops'), e => console.error('Error:', e)); // Logs: 'Error: oops', returns Err('oops')

Also available as a method:

err('oops').tapErr(e => console.error(e)); // Err('oops')

Pattern Matching

match(result, okFn, errFn)

Handles both success and failure cases, returning a single value.

import { ok, err, match } from '@deessejs/fp';

match(ok(42), x => x * 2, () => 0); // 84
match(err('oops'), x => x * 2, () => 0); // 0

Returns different types:

match(
  ok('hello'),
  value => value.length,  // string → number
  () => -1               // fallback
); // 5

Also available as a method with object syntax:

ok(42).match({ onSuccess: x => x * 2, onError: () => 0 }); // 84
err('oops').match({ onSuccess: x => x * 2, onError: () => -1 }); // -1

Combination

all(...results)

Combines multiple Results into one. Returns Ok with array of values if all succeed, or the first Err (fail-fast).

import { ok, err, all } from '@deessejs/fp';

all(ok(1), ok(2), ok(3));              // Ok([1, 2, 3])
all(ok(1), err('error'), ok(3));       // Err('error')

Swapping

swap(result)

Swaps Ok and Err. Turns Result<T, E> into Result<E, T>.

import { ok, err, swap } from '@deessejs/fp';

swap(ok(42)); // Err(42)
swap(err('oops')); // Ok('oops')

Also available as a method:

ok(42).swap(); // Err(42)
err('oops').swap(); // Ok('oops')

Unwrapping

unwrap(result)

Extracts the value if Ok, throws the error if Err.

import { ok, err, unwrap } from '@deessejs/fp';

unwrap(ok(42));      // 42
unwrap(err('oops')); // throws 'oops'

Also available as a method:

ok(42).unwrap(); // 42
err('oops').unwrap(); // throws 'oops'

Warning: Use with caution. Throws defeat the purpose of explicit error handling.

Conversions

toNullable(result)

Converts to T | null.

import { ok, err, toNullable } from '@deessejs/fp';

toNullable(ok(42));   // 42
toNullable(err('x')); // null

toUndefined(result)

Converts to T | undefined.

import { ok, err, toUndefined } from '@deessejs/fp';

toUndefined(ok(42));   // 42
toUndefined(err('x')); // undefined

Method Chaining

Result objects support chaining for fluent transformations:

import { ok, err } from '@deessejs/fp';

const result = ok(5)
  .map(x => x * 2)                      // Ok(10)
  .map(x => x + 1)                      // Ok(11)
  .flatMap(x => x > 10 ? ok(x) : err('too small')); // Err('too small')

Real-World Examples

Input Validation

For richer error handling with domain-specific errors, notes, and cause chaining, see the Error documentation.

import { ok, err, flatMap, Result } from '@deessejs/fp';

type ValidationError = { field: string; message: string };

const validateAge = (age: number): Result<number, ValidationError> => {
  if (age < 0) return err({ field: 'age', message: 'Must be positive' });
  if (age > 150) return err({ field: 'age', message: 'Must be realistic' });
  return ok(age);
};

const validateEmail = (email: string): Result<string, ValidationError> => {
  if (!email.includes('@')) return err({ field: 'email', message: 'Invalid format' });
  return ok(email);
};

const validateUser = (age: number, email: string): Result<{ age: number; email: string }, ValidationError> =>
  flatMap(validateAge(age), () =>
    flatMap(validateEmail(email), () =>
      ok({ age, email })
    )
  );

File Operations

import { readFileSync } from 'fs';
import { ok, err, getOrElse, Result } from '@deessejs/fp';

type FileError = 'NOT_FOUND' | 'PERMISSION_DENIED' | 'INVALID_JSON';

const readJsonFile = (path: string): Result<object, FileError> => {
  try {
    const content = readFileSync(path, 'utf-8');
    return ok(JSON.parse(content));
  } catch (e) {
    if (e instanceof Error) {
      if (e.message.includes('ENOENT')) return err('NOT_FOUND');
      if (e.message.includes('EACCES')) return err('PERMISSION_DENIED');
    }
    return err('INVALID_JSON');
  }
};

const config = getOrElse(readJsonFile('./config.json'), { defaults: true });

Error Discrimination

type AppError =
  | { type: 'validation'; field: string; message: string }
  | { type: 'network'; code: number }
  | { type: 'unauthorized' };

const handleError = (error: AppError): string => {
  switch (error.type) {
    case 'validation':
      return `Invalid ${error.field}: ${error.message}`;
    case 'network':
      return `Network error: ${error.code}`;
    case 'unauthorized':
      return 'Please log in';
  }
};

Comparison with Alternatives

Feature@deessejs/fpfp-tsneverthrow
Bundle size~2KB~40KB~5KB
Learning curveLowHighMedium
Dependencies0Many0

See Also

  • Maybe - For optional values (null/undefined)
  • Try - For wrapping sync functions that might throw
  • AsyncResult - For async operations with error handling
  • Error - For structured errors with enrichment

On this page