.agents/skills/define-errors/SKILL.md
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.
npx skillsauth add epicenterhq/epicenter define-errorsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Related Skills: See
error-handlingfor trySync/tryAsync usage and toast-on-error patterns. Seeservices-layerfor service architecture and namespace exports.
Use this pattern when you need to:
defineErrors.cause: unknown.extractErrorMessage(cause) inside variant factories.InferErrors/InferError.createTaggedError and split Err-pair patterns.import {
defineErrors,
extractErrorMessage,
type InferErrors,
type InferError,
} from 'wellcrafted/error';
defineErrors call — never spread them across multiple calls{ message, ...fields } — that is the entire API; no .withMessage(), .withContext(), or .withCause() chainscause: unknown is just a field like any other — accept it in the input and forward it in the return objectextractErrorMessage(cause) inside the factory, never at the call siteMyError.Variant({ ... }) returns Err(...) automatically — no separate FooErr pairInferErrors — const FooError / type FooErrorInferError<typeof FooError.Variant> to extract a single variant's type when neededService, Error, or Failed.message for end-user readability — toastOnError shows .message as the muted toast description below the bold title. Write messages that make sense to users, not just developers. Avoid raw paths, status codes, or stack traces as the primary message. Include them after a human-readable prefix:// ✅ GOOD — human-readable prefix, technical detail after
message: `Could not save recording: ${extractErrorMessage(cause)}`
// ❌ BAD — raw technical output as the entire message
message: `POST /api/recordings 500: ${extractErrorMessage(cause)}`
export const RecorderError = defineErrors({
AlreadyRecording: () => ({
message: 'A recording is already in progress',
}),
});
export type RecorderError = InferErrors<typeof RecorderError>;
// Call site
return RecorderError.AlreadyRecording();
export const DbError = defineErrors({
NotFound: ({ table, id }: { table: string; id: string }) => ({
message: `${table} '${id}' not found`,
table,
id,
}),
});
export type DbError = InferErrors<typeof DbError>;
// Call site
return DbError.NotFound({ table: 'users', id: '123' });
// error.message → "users '123' not found"
// error.table → "users"
// error.id → "123"
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>;
// Call site — pass the raw caught error, never call extractErrorMessage here
catch: (error) => FfmpegError.CompressFailed({ cause: error }),
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>;
// DeviceStreamError is automatically the union of all three variants
// Extracting a single variant type
type NoDevicesFoundError = InferError<typeof DeviceStreamError.NoDevicesFound>;
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>;
// Call site
return FsError.ReadFailed({ path: '/tmp/foo.txt', cause: error });
// Full union type for all variants
type HttpError = InferErrors<typeof HttpError>;
// Single variant type
type ConnectionError = InferError<typeof HttpError.Connection>;
// WRONG — old createTaggedError API
import { createTaggedError } from 'wellcrafted/error';
const { FooError, FooErr } = createTaggedError('FooError')
.withContext<{ id: string }>()
.withMessage(({ context }) => `Not found: ${context.id}`);
// WRONG — calling extractErrorMessage at the call site
catch: (error) => MyError.Failed({ message: extractErrorMessage(error) });
// CORRECT — pass raw cause, call extractErrorMessage inside the factory
catch: (error) => MyError.Failed({ cause: error });
// WRONG — one defineErrors per variant (defeats the namespace grouping)
const BusyError = defineErrors({ BusyError: () => ({ message: 'Busy' }) });
const PermError = defineErrors({ PermError: () => ({ message: 'No perm' }) });
// CORRECT — all variants for a domain in one call
const RecorderError = defineErrors({
Busy: () => ({ message: 'A recording is already in progress' }),
PermissionDenied: () => ({ message: 'Microphone permission denied' }),
});
// WRONG — using ReturnType instead of InferErrors
type FooError = ReturnType<typeof FooError>;
// CORRECT
type FooError = InferErrors<typeof FooError>;
// WRONG — using separate Err/FooErr pair (old API)
FooErr({ context: { id: '1' } });
// CORRECT — each variant call returns Err(...) automatically
FooError.NotFound({ id: '1' });
// WRONG — generic "Service" variant name (says nothing about the failure mode)
const RecorderError = defineErrors({
Service: ({ message }: { message: string }) => ({ message }),
});
// RecorderError.Service({ message: '...' }) — "Service" is not a failure mode
// CORRECT — name each variant by what actually went wrong
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,
}),
});
// WRONG — generic catch-all with operation string (hides failure modes behind a parameter)
const FfmpegError = defineErrors({
Service: ({ operation, cause }: { operation: string; cause: unknown }) => ({
message: `Failed to ${operation}: ${extractErrorMessage(cause)}`,
operation,
cause,
}),
});
// FfmpegError.Service({ operation: 'compress audio', cause }) — variant name is meaningless
// CORRECT — each operation is its own variant
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,
}),
});
// WRONG — monolithic single-variant error for a domain with many failure modes
const RecorderError = defineErrors({
Error: ({ message }: { message: string }) => ({ message }), // Too vague
});
// CORRECT — split by failure mode
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,
}),
});
documentation
Yjs CRDT patterns, shared types (Y.Map, Y.Array, Y.Text), conflict resolution, and document storage. Use when the user mentions Yjs, Y.Doc, CRDTs, collaborative editing, or when handling shared types, implementing real-time sync, or optimizing document storage.
tools
Voice and tone rules for all written content—prose, UI text, tooltips, error messages. Use when the user says "fix the tone", "rewrite this", "sounds like AI", "sounds corporate", or when writing any user-facing text, landing pages, product copy, or open-source documentation.
tools
Workspace API patterns for defineTable, defineKv, versioning, migrations, data access (CRUD + observation), withActions, and extension ordering. Use when the user mentions workspace, defineTable, defineKv, createWorkspace, withActions, withExtension, defineQuery, defineMutation, connectWorkspace, or when defining schemas, reading/writing table data, observing changes, writing migrations, chaining extensions, or attaching actions to a workspace client.
documentation
Standard workflow for implementing features with specs and planning documents. Use when the user says "start a new feature", "how should I plan this", "what's the process", or when starting implementation, planning work, or working on any non-trivial task.