@deessejs/fp

Error

Structured domain errors with enrichment, chaining, and Zod validation

The Error system provides rich error objects inspired by Python's exception classes. Errors have structured metadata, support enrichment with notes and cause chaining, and integrate seamlessly with Result.

Why Error?

JavaScript's native Error is limited. It only has a message and stack trace. The Error system adds:

  • Structured args - domain-specific error data
  • Notes - additional context
  • Cause chaining - trace error provenance
  • Zod validation - validate error arguments at runtime
  • Enrichment methods - add notes, chain causes

Error vs Result

AspectResultError
PurposeRepresent success/failureRepresent domain error
Methodsmap, flatMap, getOrElseaddNotes, from
ChainingRailway-oriented programmingError provenance tracking
With ZodNo validationSchema validation
import { error, err, ok } from '@deessejs/fp';

// Error is a plain error object
const SizeError = error({
  name: 'SizeError',
  schema: z.object({
    current: z.number(),
    wanted: z.number(),
  }),
});

const domainError = SizeError({ current: 3, wanted: 5 });
domainError.name;     // 'SizeError'
domainError.args;     // { current: 3, wanted: 5 }
domainError.notes;    // []
domainError.cause;    // None (until enriched)

// Wrap with err() to get Result methods
const result = err(domainError);
result.ok === false;
result.error === domainError; // reference, not self

Core Concepts

Error Properties

An Error<T> has these properties:

PropertyTypeDescription
namestringError type identifier
argsTDomain-specific error data
notesreadonly string[]Additional context
causeMaybe<Error>Original error (chained)
messagestringHuman-readable message
stackstringStack trace

ErrorBuilder

A function that creates Error<T> instances:

type ErrorBuilder<T = object> = (args?: T) => Error<T>;

Accessing Cause

The cause field is Maybe<Error>. Use Maybe methods to access it:

// Get cause name with default
error.cause.map(c => c.name).getOrElse('no cause');

// Check if cause exists
if (error.cause.isSome()) {
  console.log(error.cause.value.name);
}

// Chain through nested causes
error.cause
  .flatMap(c => c.cause)
  .map(c => c.name)
  .getOrElse('no cause');

Reference

Creating Errors

error(options)

Creates an ErrorBuilder. Schema is optional.

import { z } from 'zod';
import { error } from '@deessejs/fp';

// With schema - validates arguments
const SizeError = error({
  name: 'SizeError',
  schema: z.object({
    current: z.number(),
    wanted: z.number(),
  }),
});

// Without schema
const SimpleError = error({
  name: 'SimpleError',
});

Options:

OptionTypeDescription
namestringError class name
schemaZodSchema<T>Zod schema for args validation (optional)
message(args: T) => stringCustom message function

Creating Error Instances

Call the ErrorBuilder to create instances:

// With schema - validates arguments
const domainError = SizeError({ current: 3, wanted: 5 });

// Without schema - args are optional
const simpleError = SimpleError();     // No args needed
const alsoWorks = SimpleError({});     // Also valid

Enrichment

addNotes(...notes)

Add contextual notes to an error:

const e = SizeError({ current: 3, wanted: 5 })
  .addNotes('Attempted to process file', 'User: john');

e.notes; // ['Attempted to process file', 'User: john']

from(cause)

Chain the cause of an error. Accepts Error, Err<Error>, or Maybe<Error>:

const NetworkError = error({
  name: 'NetworkError',
  schema: z.object({ host: z.string() }),
});

const networkError = NetworkError({ host: 'api.example.com' });

// From Error object
const e = SizeError({ current: 3, wanted: 5 }).from(networkError);

// From Result containing an Error
const result = err(networkError);
const e2 = SizeError({ current: 3, wanted: 5 }).from(result);

// From Maybe<Error>
const maybeError = some(networkError);
const e3 = SizeError({ current: 3, wanted: 5 }).from(maybeError);

Combining Enrichments

const e = SizeError({ current: 3, wanted: 5 })
  .addNotes('Processing file: data.json')
  .from(networkError)
  .addNotes('File processing failed');

Validation

When a schema is provided, arguments are automatically validated:

SizeError({ current: 'not a number' });
// Returns Error with name 'SizeErrorValidationError'

Custom message function:

const ValidationError = error({
  name: 'ValidationError',
  schema: z.object({ field: z.string() }),
  message: (args) => `Field "${args.field}" is invalid`,
});

ErrorGroups

exceptionGroup(errors)

Group multiple errors together:

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

const errors = exceptionGroup([
  SizeError({ current: 3, wanted: 5 }),
  ValidationError({ field: 'email' }),
]);

errors.name;         // 'ExceptionGroup'
errors.exceptions;   // [SizeError, ValidationError]

Guards

isError(value)

Type guard that checks if a value is an Error:

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

isError(SizeError({ current: 3, wanted: 5 })); // true
isError('not an error');                       // false
isError(null);                                // false

isErrorGroup(value)

Type guard that checks if a value is an ErrorGroup:

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

isErrorGroup(exceptionGroup([...])); // true
isErrorGroup(SizeError({...}));     // false

Assertions

assertIsError(value)

Assertion function that throws if value is not an Error:

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

assertIsError(value);
// Throws TypeError if not an Error
// value is Error here, TypeScript knows the type

assertIsErrorGroup(value)

Assertion function that throws if value is not an ErrorGroup:

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

assertIsErrorGroup(value);
// Throws TypeError if not an ErrorGroup

Utilities

getErrorMessage(error)

Extract a human-readable message:

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

getErrorMessage(SizeError({ current: 3, wanted: 5 }));
// Returns 'SizeError: {"current":3,"wanted":5}'

getErrorMessage(exceptionGroup([...]));
// Returns 'ExceptionGroup: 2 error(s)'

flattenErrorGroup(group)

Get all errors from a group (including nested):

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

flattenErrorGroup(group); // Error[]

filterErrorsByName(group, name)

Find errors by name in a group:

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

filterErrorsByName(group, 'SizeError'); // SizeError[]
filterErrorsByName(group, 'NetworkError'); // NetworkError[]

raise()

raise(error)

Throws the error and returns never. Use only for unrecoverable programmer errors:

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

// Only for unrecoverable errors - things that indicate a bug
const process = (size: number): Result<Data, Error> => {
  if (size > MAX_SIZE) {
    raise(SizeError({ current: size, wanted: MAX_SIZE }));
  }
  return ok({ size });
};

When NOT to use raise():

// Bad - breaks the rail for expected failures
const validateBad = (input: string): Result<string, Error> => {
  if (!input) raise(EmptyInputError({}));
  return ok(input);
};

// Good - error travels through the rail
const validate = (input: string): Result<string, Error> => {
  if (!input) return err(EmptyInputError({}));
  return ok(input);
};

Using with Result

Error objects integrate with Result via err():

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

const result = ok(10).flatMap((x) => {
  if (x > 5) {
    return err(SizeError({ current: x, wanted: 5 }));
  }
  return ok(x * 2);
});

result.mapErr((e) => console.log(e.name));

Type Compatibility

Error<T> is compatible with JavaScript's native Error:

const e = SizeError({ current: 3, wanted: 5 });

// Error is compatible with globalThis.Error
const nativeErrorHandler = (e: Error) => {
  console.log(e.message, e.stack);
};

nativeErrorHandler(SizeError({...}));

See Also

  • Result - For success/failure with error chaining
  • Maybe - For optional values (null/undefined)
  • Try - For wrapping sync functions that might throw
  • AsyncResult - For async operations with error handling

On this page