errors
Original:🇺🇸 English
Translated
[Pragmatic DDD Architecture] Guide for creating strictly typed Domain validation and infrastructure errors. Use when handling or creating new errors to ensure they conform to the Railway-oriented programming model (neverthrow Result), TypeScript error branding, and constructor parameter constraints. Covers co-location rules vs. shared domain error usage.
11installs
Sourceleif-sync/pragmatic-ddd
Added on
NPX Install
npx skill4agent add leif-sync/pragmatic-ddd errorsTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →DDD Error Management
1. Core Principles (RFC 2119)
Errors in this architecture are never "thrown" as standard exceptions during domain validation. They are instead strictly typed and returned as failures inside 's .
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
neverthrowResult<T, E>- No Enums: You MUST NOT use or string union types to represent different error cases or mappings natively within the Use Cases/Domain (Actions can, but Domain MUST NOT). Each domain error MUST be a distinct class extending
enum.Error - TypeScript Error Branding: Every custom error class MUST include using a
declare private readonly [unique]: never. This strictly prevents duck-typing issues and allows 100% safeunique symbolchecks when exhaustively resolving the error viainstanceofin the Server Action layer.assertNever - Payload Objects: Except for wrapping native errors (e.g., Node.js or Database instances), constructors MUST ALWAYS accept a single
Errorobject argument for dependencies/properties, even if it only has one property.params: { ... } - Strict Encapsulation: Internal properties capturing the invalid value or context MUST be typed as Value Objects (or Branded primitives). They MUST be with explicit
private readonlygetters. You MUST NOT expose properties directly via.get...()access modifiers.public - Message Formatting: When building the message string inside the constructor, you MUST extract the primitive from the Value Object using its
super(...)method (or its semantic equivalent like.toBranded())..getEncoded()
2. Location & Granularity
- Local Errors (Co-location): If an error is strictly limited to validating a single Value Object (e.g., ), define the error in the exact same file as the Value Object. If an error is only emitted by a specific Use Case parsing logic, define it with the Use Case.
InvalidEmailError - Shared Domain Errors: If an error spans multiple entities, use cases, or represents a core domain rule (e.g., ,
FolderAlreadyExistsError), define it in the shared domain errors location as dictated by the architecture skill.UserNotFoundError
3. Creating Domain Errors (Business Rules)
This format strictly applies for any rules related to validation, business constraints, or entity invariants. Notice the object parameter payload .
params: { ... }typescript
import { FolderName } from "../value-objects/folderName";
declare const unique: unique symbol;
export class FolderAlreadyExistsError extends Error {
// BRANDING: This ensures TypeScript treats this type strictly.
declare private readonly [unique]: never;
// MUST store the actual Value Object privately
private readonly folderName: FolderName;
constructor(params: { folderName: FolderName }) {
// MUST extract the primitive value via `.toBranded()` for message formatting
super(`Folder with name "${params.folderName.toBranded()}" already exists.`);
this.folderName = params.folderName;
}
// MUST return the Value Object
getFolderName(): FolderName {
return this.folderName;
}
}4. Creating Infrastructure Errors (Database/External)
When catching external exceptions (e.g., Drizzle/Postgres errors), the objective is to wrap the native inside our branded Domain Architecture. This is the only time the constructor argument should be an instance.
ErrorErrortypescript
declare const unique: unique symbol;
export class RepositoryError extends Error {
declare private readonly [unique]: never;
constructor(error: Error) {
const { message, ...rest } = error;
super(message, rest);
}
}5. Usage in the Application Flow
In your business logic—like Repositories or Use Cases—these classes are always used inside pure functions returning instead of .
Result.err()throwtypescript
// ✅ RECOMMENDED - Railway flow
if (folders.length > 0) {
return err(new FolderAlreadyExistsError({ folderName }));
}
// ❌ MUST NOT - Never throw domain validation exceptions directly
if (folders.length > 0) {
throw new FolderAlreadyExists({ folderName });
}6. References
For examples based on existing models, review real project implementations stored in the folder:
.agents/skills/errors/references/- workspaceNotFoundError.md - Standard domain error taking a single Value Object parameter.
- pathAlreadyInUseError.md - Domain error showcasing optional parameters and conditional messages.