| name | define-errors |
| description | Define typed, serializable error variants with defineErrors from wellcrafted. Use when creating error types, handling domain errors, or reviewing error definitions. |
defineErrors
import {
defineErrors,
extractErrorMessage,
type InferErrors,
type InferError,
} from 'wellcrafted/error';
Core Rules
- All variants for a domain live in one
defineErrors call — never spread across multiple calls
- The factory returns
{ message, ...fields } — no .withMessage() or .withContext() chains
cause: unknown is a field like any other — accept it in input, forward in return
- Call
extractErrorMessage(cause) inside the factory, never at the call site
- Each call like
MyError.Variant({ ... }) returns Err(...) automatically
- Shadow the const with a same-name type using
InferErrors
- Use
InferError<typeof MyError.Variant> for a single variant's type
- Variant names describe the specific failure mode — never
Service, Error, or Failed
- Aim for 2–5 variants per domain, each named by failure mode
See also: result-types skill for trySync/tryAsync wrapping patterns.
Patterns
Zero-arg variant — static message
const UserError = defineErrors({
AlreadyExists: () => ({
message: 'A user with this email already exists',
}),
});
type UserError = InferErrors<typeof UserError>;
return UserError.AlreadyExists();
Structured fields — message computed from input
const DbError = defineErrors({
NotFound: ({ table, id }: { table: string; id: string }) => ({
message: `${table} '${id}' not found`,
table,
id,
}),
});
type DbError = InferErrors<typeof DbError>;
return DbError.NotFound({ table: 'users', id: '123' });
Cause wrapping — extractErrorMessage inside the factory
import { extractErrorMessage } from 'wellcrafted/error';
const FileError = defineErrors({
ReadFailed: ({ path, cause }: { path: string; cause: unknown }) => ({
message: `Failed to read '${path}': ${extractErrorMessage(cause)}`,
path,
cause,
}),
WriteFailed: ({ path, cause }: { path: string; cause: unknown }) => ({
message: `Failed to write '${path}': ${extractErrorMessage(cause)}`,
path,
cause,
}),
});
type FileError = InferErrors<typeof FileError>;
catch: (error) => FileError.ReadFailed({ path: '/tmp/config.json', cause: error }),
Multiple variants — discriminated union built-in
const HttpError = defineErrors({
Connection: ({ url, cause }: { url: string; cause: unknown }) => ({
message: `Failed to connect to ${url}: ${extractErrorMessage(cause)}`,
url,
cause,
}),
Timeout: ({ url, ms }: { url: string; ms: number }) => ({
message: `Request to ${url} timed out after ${ms}ms`,
url,
ms,
}),
Response: ({ status, body }: { status: number; body: unknown }) => ({
message: `HTTP ${status}: ${extractErrorMessage(body)}`,
status,
body,
}),
});
type HttpError = InferErrors<typeof HttpError>;
type TimeoutError = InferError<typeof HttpError.Timeout>;
Composing errors across layers
Each layer defines its own error vocabulary. Inner errors become cause fields in higher-level errors.
const HttpError = defineErrors({
Connection: ({ url, cause }: { url: string; cause: unknown }) => ({
message: `Failed to connect to ${url}: ${extractErrorMessage(cause)}`,
url,
cause,
}),
});
const UserError = defineErrors({
FetchFailed: ({ userId, cause }: { userId: string; cause: unknown }) => ({
message: `Failed to fetch user ${userId}: ${extractErrorMessage(cause)}`,
userId,
cause,
}),
});
const { data, error } = await tryAsync({
try: () => fetch(`/api/users/${userId}`),
catch: (cause) => UserError.FetchFailed({ userId, cause }),
});
See also: patterns skill for service layer architecture with error composition.
Type Extraction
type HttpError = InferErrors<typeof HttpError>;
type ConnectionError = InferError<typeof HttpError.Connection>;
Anti-Patterns
One defineErrors per variant
const NotFoundError = defineErrors({ NotFound: () => ({ message: 'Not found' }) });
const TimeoutError = defineErrors({ Timeout: () => ({ message: 'Timed out' }) });
const HttpError = defineErrors({
NotFound: () => ({ message: 'Not found' }),
Timeout: () => ({ message: 'Timed out' }),
});
extractErrorMessage at the call site
catch: (error) => MyError.Failed({ message: extractErrorMessage(error) });
catch: (error) => MyError.Failed({ cause: error });
Generic variant names
const UserError = defineErrors({
Service: ({ message }: { message: string }) => ({ message }),
});
const UserError = defineErrors({
AlreadyExists: ({ email }: { email: string }) => ({
message: `User ${email} already exists`,
email,
}),
CreateFailed: ({ cause }: { cause: unknown }) => ({
message: `Failed to create user: ${extractErrorMessage(cause)}`,
cause,
}),
});
Monolithic catch-all variant
const DbError = defineErrors({
Failed: ({ operation, cause }: { operation: string; cause: unknown }) => ({
message: `Failed to ${operation}: ${extractErrorMessage(cause)}`,
operation,
cause,
}),
});
const DbError = defineErrors({
QueryFailed: ({ cause }: { cause: unknown }) => ({
message: `Query failed: ${extractErrorMessage(cause)}`,
cause,
}),
InsertFailed: ({ cause }: { cause: unknown }) => ({
message: `Insert failed: ${extractErrorMessage(cause)}`,
cause,
}),
});
Discriminated union inputs (sub-discriminants)
const FormError = defineErrors({
Invalid: (input: {
reason: 'bad_email' | 'weak_password' | 'mismatch';
value?: string;
}) => ({
message: { bad_email: `Invalid email: '${input.value}'`, }[input.reason],
...input,
}),
});
const FormError = defineErrors({
InvalidEmail: ({ value }: { value: string }) => ({
message: `Invalid email: '${value}'`,
value,
}),
WeakPassword: () => ({
message: 'Password must be at least 8 characters',
}),
PasswordMismatch: () => ({
message: 'Passwords do not match',
}),
});
Conditional logic in factories
If the factory branches on its inputs to decide the message, each branch should be its own variant. The branching is evidence that multiple errors are hiding in one.
Using ReturnType instead of InferErrors
type MyError = ReturnType<typeof MyError>;
type MyError = InferErrors<typeof MyError>;