| name | define-errors |
| description | How to define and use defineErrors from wellcrafted. Use when creating new error types, updating error definitions, or reviewing error patterns. Covers variant factories, extractErrorMessage for cause, InferErrors/InferError for types, and call site patterns. |
| metadata | {"author":"epicenter","version":"3.0"} |
defineErrors
Import
import {
defineErrors,
extractErrorMessage,
type InferErrors,
type InferError,
} from 'wellcrafted/error';
Core Rules
- All variants for a domain live in one
defineErrors call — never spread them across multiple calls
- The factory function returns
{ message, ...fields } — that is the entire API; no .withMessage(), .withContext(), or .withCause() chains
cause: unknown is just a field like any other — accept it in the input and forward it in the return object
- Call
extractErrorMessage(cause) inside the factory, never at the call site
- Each call like
MyError.Variant({ ... }) returns Err(...) automatically — no separate FooErr pair
- Shadow the const with a same-name type using
InferErrors — const FooError / type FooError
- Use
InferError<typeof FooError.Variant> to extract a single variant's type when needed
- Variant names describe the specific failure mode — never use generic names like
Service, Error, or Failed
- Aim for 2–5 variants per domain, each named by failure mode
Patterns
1. Simple variant — no input, static message
export const RecorderError = defineErrors({
AlreadyRecording: () => ({
message: 'A recording is already in progress',
}),
});
export type RecorderError = InferErrors<typeof RecorderError>;
return RecorderError.AlreadyRecording();
2. Variant with structured fields — message computed from input
export const DbError = defineErrors({
NotFound: ({ table, id }: { table: string; id: string }) => ({
message: `${table} '${id}' not found`,
table,
id,
}),
});
export type DbError = InferErrors<typeof DbError>;
return DbError.NotFound({ table: 'users', id: '123' });
3. Variant with cause — extractErrorMessage inside the factory
import { extractErrorMessage } from 'wellcrafted/error';
export const FfmpegError = defineErrors({
CompressFailed: ({ cause }: { cause: unknown }) => ({
message: `Failed to compress audio: ${extractErrorMessage(cause)}`,
cause,
}),
VerifyFailed: ({ cause }: { cause: unknown }) => ({
message: `Failed to verify temp file: ${extractErrorMessage(cause)}`,
cause,
}),
});
export type FfmpegError = InferErrors<typeof FfmpegError>;
catch: (error) => FfmpegError.CompressFailed({ cause: error }),
4. Multiple variants in one object — discriminated union built-in
export const DeviceStreamError = defineErrors({
PermissionDenied: ({ cause }: { cause: unknown }) => ({
message: `Microphone permission denied. ${extractErrorMessage(cause)}`,
cause,
}),
DeviceConnectionFailed: ({
deviceId,
cause,
}: {
deviceId: string;
cause: unknown;
}) => ({
message: `Unable to connect to device '${deviceId}'. ${extractErrorMessage(cause)}`,
deviceId,
cause,
}),
NoDevicesFound: () => ({
message: "No microphones found. Check your connections and try again.",
}),
});
export type DeviceStreamError = InferErrors<typeof DeviceStreamError>;
type NoDevicesFoundError = InferError<typeof DeviceStreamError.NoDevicesFound>;
5. Domain errors with specific operation failures
export const FsError = 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,
}),
DeleteFailed: ({ path, cause }: { path: string; cause: unknown }) => ({
message: `Failed to delete '${path}': ${extractErrorMessage(cause)}`,
path,
cause,
}),
});
export type FsError = InferErrors<typeof FsError>;
return FsError.ReadFailed({ path: '/tmp/foo.txt', cause: error });
Type Extraction
type HttpError = InferErrors<typeof HttpError>;
type ConnectionError = InferError<typeof HttpError.Connection>;
Anti-Patterns
import { createTaggedError } from 'wellcrafted/error';
const { FooError, FooErr } = createTaggedError('FooError')
.withContext<{ id: string }>()
.withMessage(({ context }) => `Not found: ${context.id}`);
catch: (error) => MyError.Failed({ message: extractErrorMessage(error) });
catch: (error) => MyError.Failed({ cause: error });
const BusyError = defineErrors({ BusyError: () => ({ message: 'Busy' }) });
const PermError = defineErrors({ PermError: () => ({ message: 'No perm' }) });
const RecorderError = defineErrors({
Busy: () => ({ message: 'A recording is already in progress' }),
PermissionDenied: () => ({ message: 'Microphone permission denied' }),
});
type FooError = ReturnType<typeof FooError>;
type FooError = InferErrors<typeof FooError>;
FooErr({ context: { id: '1' } });
FooError.NotFound({ id: '1' });
const RecorderError = defineErrors({
Service: ({ message }: { message: string }) => ({ message }),
});
const RecorderError = defineErrors({
AlreadyRecording: () => ({ message: 'A recording is already in progress' }),
PermissionDenied: ({ cause }: { cause: unknown }) => ({
message: `Microphone permission denied. ${extractErrorMessage(cause)}`,
cause,
}),
DeviceNotFound: ({ deviceId }: { deviceId: string }) => ({
message: `Device not found: ${deviceId}`,
deviceId,
}),
});
const FfmpegError = defineErrors({
Service: ({ operation, cause }: { operation: string; cause: unknown }) => ({
message: `Failed to ${operation}: ${extractErrorMessage(cause)}`,
operation,
cause,
}),
});
const FfmpegError = defineErrors({
CompressFailed: ({ cause }: { cause: unknown }) => ({
message: `Failed to compress audio: ${extractErrorMessage(cause)}`,
cause,
}),
VerifyFailed: ({ cause }: { cause: unknown }) => ({
message: `Failed to verify temp file: ${extractErrorMessage(cause)}`,
cause,
}),
});
const RecorderError = defineErrors({
Error: ({ message }: { message: string }) => ({ message }),
});
const RecorderError = defineErrors({
AlreadyRecording: () => ({ message: 'A recording is already in progress' }),
InitFailed: ({ cause }: { cause: unknown }) => ({
message: `Failed to initialize recorder: ${extractErrorMessage(cause)}`,
cause,
}),
StreamAcquisition: ({ cause }: { cause: unknown }) => ({
message: `Failed to acquire recording stream: ${extractErrorMessage(cause)}`,
cause,
}),
});