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 because it only has a message and stack trace. The Error system adds:

  • Structured args -- Domain-specific error data
  • Notes -- Additional context that you can add later
  • Cause chaining -- Trace error provenance through the call stack
  • Zod validation -- Validate error arguments at runtime
  • Sensitive data redaction -- Automatic redaction in message and stack

Error vs Result

AspectResultError
PurposeRepresent success or failureRepresent domain error
Methodsmap, flatMap, getOrElseaddNotes, from
ChainingRailway-oriented programmingError provenance tracking
With ZodNo validationSchema validation

This code defines a custom error type with a Zod schema. The schema validates the error arguments at creation time and the message function receives those arguments to generate a human-readable string.

defining-error-types.ts
import { error, err, ok, some } from '@deessejs/fp';
import { z } from 'zod';

// Define an error type with schema
const SizeError = error({
  name: 'SizeError',
  schema: z.object({
    current: z.number(),
    wanted: z.number(),
  }),
  message: (args) => `Size error: got ${args.current}, wanted ${args.wanted}`,
});

// Create an error instance
const domainError = SizeError({ current: 3, wanted: 5 });
domainError.name;     // 'SizeError'
domainError.args;     // { current: 3, wanted: 5 }
domainError.notes;    // []
domainError.cause;    // None (until enriched)
domainError.message;  // 'Size error: got 3, wanted 5'

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

Core Concepts

Error Properties

An Error has these properties:

PropertyTypeDescription
namestringError type identifier
_tagstringTag for pattern matching (same as name)
argsTDomain-specific error data
notesreadonly string[]Additional context
causeMaybe ErrorOriginal error (chained)
messagestringHuman-readable message
stackstringStack trace

Creating Errors

The error Builder Function

This function creates an ErrorBuilder, which is a factory for generating typed errors. The schema is optional but recommended because it enables runtime validation of error arguments.

creating-an-error-builder.ts
import { z } from 'zod';
import { error } from '@deessejs/fp';

// With schema - validates arguments at creation
const NotFoundError = error({
  name: 'NotFoundError',
  schema: z.object({
    id: z.string(),
    resource: z.string().optional(),
  }),
  message: (args) =>
    args.resource
      ? `${args.resource} not found: ${args.id}`
      : `Resource not found: ${args.id}`,
});

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

The error builder accepts these options:

OptionTypeDescription
namestringError class name. This is required and identifies the error type.
schemaZodSchema TZod schema for args validation. When provided, arguments are validated at creation and a ZodError is thrown if validation fails.
messagefunction that takes args and returns stringCustom message function that receives the validated args and returns a human-readable message.
codenumberNumeric error code that you can use for error categorization, such as HTTP status codes (for example, 404).

Creating Error Instances

You call the ErrorBuilder to create instances of the error. When a schema is defined, arguments are validated before the error is created.

creating-error-instances.ts
// With schema - validates arguments
const notFound = NotFoundError({ id: '123', resource: 'user' });
// notFound.message === 'Resource not found: user'

// Without schema
const simple = SimpleError();

Enrichment

Adding Notes

You use addNotes to attach contextual information to an error. This is useful for adding debugging context or tracking information as the error propagates through your code.

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

e.notes; // ['Attempted to process file', 'User: john']
e.message; // 'Size error: got 3, wanted 5'

Chaining Cause

You use the from method to chain errors, which preserves the original error as the cause. This creates a linked list of errors that lets you trace the full error chain back to its origin. The from method accepts an Error, a Result containing an Error, or a Maybe containing an Error.

chaining-errors.ts
import { error, err, some } from '@deessejs/fp';

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

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

// Chain from an Error
const e1 = SizeError({ current: 3, wanted: 5 }).from(networkError);
e1.cause; // Some(NetworkError)

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

// Chain from Maybe Error
const maybeError = some(networkError);
const e3 = SizeError({ current: 3, wanted: 5 }).from(maybeError);
e3.cause; // Some(NetworkError)

// Accessing the cause
e1.cause.map(c => c.name).getOrElse('no cause'); // 'NetworkError'

Combining Enrichments

You can chain multiple enrichment methods together to build a detailed error chain with notes at each step.

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

Pattern Matching

Pattern Matching on Errors

The matchError function provides exhaustive pattern matching on errors. You pass the error and a map of handlers keyed by error name, and it returns the result of the matching handler. The function requires that all possible error types have corresponding handlers.

pattern-matching-errors.ts
import { ok, err, error, matchError } from '@deessejs/fp';
import { z } from 'zod';

const NotFoundError = error({
  name: 'NotFoundError',
  schema: z.object({ id: z.string(), resource: z.string() }),
});

const ValidationError = error({
  name: 'ValidationError',
  schema: z.object({ field: z.string(), message: z.string() }),
});

const result = err(NotFoundError({ id: '123', resource: 'user' }));

const message = matchError(result.error, {
  NotFoundError: (e) => `Missing ${e.resource}: ${e.id}`,
  ValidationError: (e) => `Invalid ${e.field}: ${e.message}`,
});
// message === 'Missing user: 123'

Partial Pattern Matching

The matchErrorPartial function allows non-exhaustive pattern matching by accepting a default handler that runs when no specific handler matches.

partial-pattern-matching.ts
const message = matchErrorPartial(
  result.error,
  {
    NotFoundError: (e) => `Missing: ${e.id}`,
    ValidationError: (e) => `Invalid: ${e.field}`,
  },
  (e) => `Unknown error: ${e._tag}`
);

Type Guards

Static Type Guard on Builders

Each error builder includes a static is method that acts as a type guard. This lets you narrow types safely when you need to check the specific error type at runtime.

using-type-guard.ts
const notFound = NotFoundError({ id: '123' });

if (NotFoundError.is(notFound)) {
  notFound.id;  // Fully narrowed to string
  notFound.resource; // string | undefined (based on schema)
}

Generic Error Type Guard

The isError function checks if any value is an Error from this library. This is useful when you want to handle errors generically without knowing their specific type.

generic-error-check.ts
import { isError } from '@deessejs/fp';

if (isError(err)) {
  console.log(err.name);  // All errors have name
  console.log(err.args);  // All errors have args
}

Tagged Error Check

The TaggedError.is method checks if a value is any kind of tagged error. Tagged errors have a _tag property that serves as a discriminator.

tagged-error-check.ts
import { TaggedError } from '@deessejs/fp';

if (TaggedError.is(result.error)) {
  result.error._tag; // string - the discriminator
}

Sensitive Data Handling

Errors automatically redact sensitive fields in message and stack. This protects sensitive data such as passwords and API keys from being exposed in error logs or user-facing messages. The redaction happens when the message is generated, while the args object retains the original values so you can access them programmatically.

sensitive-data-redaction.ts
const AuthError = error({
  name: 'AuthError',
  schema: z.object({
    userId: z.string(),
    password: z.string(),
    token: z.string(),
    apiKey: z.string(),
  }),
});

const err = AuthError({
  userId: '123',
  password: 'super_secret_123',
  token: 'ghp_xxxxx',
  apiKey: 'sk-xxxxx',
});

err.message;
// "AuthError: {userId:123, password:[REDACTED], token:[REDACTED], apiKey:[REDACTED]}"

err.args.password; // "super_secret_123" - NOT redacted in args

The following field names are automatically redacted: password, token, secret, apiKey, authorization, credential, private, and other fields that match common sensitive data patterns.

Redaction only applies to message and stack, not to args. This allows you to log errors while protecting sensitive data in output.

Validation

When a schema is provided, arguments are validated at creation time. If validation fails, a ZodError is thrown wrapped in a ValidationError with the original ZodError as its cause. This ensures that invalid error arguments are caught early rather than causing issues later.

schema-validation.ts
// Valid creation
const valid = ValidationError({ field: 'email', message: 'invalid format' });

// Invalid - throws ZodError wrapped in a ValidationError
try {
  ValidationError({ field: 123 }); // field must be string
} catch (e) {
  // e is a ValidationError with cause being the ZodError
}

Error Groups

Grouping Multiple Errors

The exceptionGroup function groups multiple errors together into a single container. This is useful when you want to collect several related errors and handle them as a unit.

grouping-errors.ts
import { exceptionGroup, error } from '@deessejs/fp';
import { z } from 'zod';

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

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

const errors = exceptionGroup([
  ParseError({ input: 'invalid-json' }),
  ValidationError({ field: 'email' }),
]);

errors.name;      // 'ExceptionGroup'
errors.errors;    // [ParseError, ValidationError]

Accessing Group Errors

When you catch an ExceptionGroup, you can access the individual errors through the errors property and check the count through the message.

accessing-group-errors.ts
try {
  // code that throws exceptionGroup
} catch (e) {
  if (ExceptionGroup.is(e)) {
    e.errors;  // Array of errors in the group
    e.message; // "ExceptionGroup: 2 error(s)"
  }
}

Guards and Utilities

Checking if a Value is an Error

The isError function is a type guard that checks if a value is an Error from this library. It returns true for any error created with the error builder.

iserror-guard.ts
import { isError } from '@deessejs/fp';

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

Extracting Error Messages

The getErrorMessage function extracts a human-readable message from any error. This is useful when you need a simple string representation without dealing with the full error structure.

geterrormessage.ts
import { getErrorMessage } from '@deessejs/fp';

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

Flattening Error Groups

The flattenErrorGroup function recursively flattens nested error groups into a single flat array of errors. This is useful when you want to process all errors in a group regardless of nesting.

flattenerrrorgroup.ts
import { flattenErrorGroup } from '@deessejs/fp';

flattenErrorGroup(group); // Error[]

Filtering Errors by Name

The filterErrorsByName function finds all errors with a specific name in an error group. This is useful when you want to handle specific error types separately from the group.

filtererrorsbyname.ts
import { filterErrorsByName } from '@deessejs/fp';

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

Using with Result

Error objects integrate with Result via the err function. When you wrap an error in err, you get access to Result methods like mapErr and flatMap while preserving the error reference.

error-with-result.ts
import { ok, err, map } 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));

Direct Yield in Result.gen

In Result.gen generators, you can yield errors directly without wrapping them in err. The generator handles the conversion automatically, which makes the code cleaner when using generator syntax for async operations.

direct-yield-in-result-gen.ts
const result = await Result.gen(async function* () {
  const user = yield* ok({ id: '123', name: 'Alice', active: true });

  if (!user.active) {
    yield* ValidationError({ field: 'user.active', message: 'User is inactive' });
  }

  return user;
});

The raise Function

You use raise to throw an error and return never. This function should only be used for unrecoverable programmer errors, such as violations of internal invariants that indicate bugs rather than expected failure cases. The raise function immediately throws and thus short-circuits any further execution.

using-raise.ts
import { raise } from '@deessejs/fp';
import { ok } 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 to Avoid raise

You should not use raise for expected failures because it breaks the railway-oriented programming flow. Expected failures should travel through the Result rail using err instead, which preserves the error for proper handling by callers.

avoiding-raise.ts
// 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);
};

See Also

On this page