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
| Aspect | Result | Error |
|---|---|---|
| Purpose | Represent success or failure | Represent domain error |
| Methods | map, flatMap, getOrElse | addNotes, from |
| Chaining | Railway-oriented programming | Error provenance tracking |
| With Zod | No validation | Schema 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.
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 copyCore Concepts
Error Properties
An Error has these properties:
| Property | Type | Description |
|---|---|---|
| name | string | Error type identifier |
| _tag | string | Tag for pattern matching (same as name) |
| args | T | Domain-specific error data |
| notes | readonly string[] | Additional context |
| cause | Maybe Error | Original error (chained) |
| message | string | Human-readable message |
| stack | string | Stack 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.
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:
| Option | Type | Description |
|---|---|---|
| name | string | Error class name. This is required and identifies the error type. |
| schema | ZodSchema T | Zod schema for args validation. When provided, arguments are validated at creation and a ZodError is thrown if validation fails. |
| message | function that takes args and returns string | Custom message function that receives the validated args and returns a human-readable message. |
| code | number | Numeric 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.
// 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.
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.
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.
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.
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.
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.
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.
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.
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.
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 argsThe 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.
// 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.
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.
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.
import { isError } from '@deessejs/fp';
isError(SizeError({ current: 3, wanted: 5 })); // true
isError('not an error'); // falseExtracting 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.
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.
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.
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.
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.
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.
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.
// 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);
};