Skip to main content

ValidationError

ValidationError is the standard error type thrown when validation fails. It contains structured information about all validation issues.
import { ValidationError } from "@woltz/rich-domain";

try {
  const user = new User({ name: "", email: "invalid" });
} catch (error) {
  if (ValidationError.isValidationError(error)) {
    console.log(error.message);
    // "Validation failed: Name is required, Invalid email format"

    console.log(error.issues);
    // [
    //   { path: ["name"], message: "Name is required" },
    //   { path: ["email"], message: "Invalid email format" }
    // ]
  }
}

ValidationError API

Properties

interface ValidationError extends Error {
  name: "ValidationError";
  message: string; // Summary of all errors
  issues: ValidationIssue[]; // Detailed error list
}

interface ValidationIssue {
  path: string[]; // Field path (e.g., ["address", "city"])
  message: string; // Error message
}

Methods

getMessages()

Get all error messages as a flat array:
const messages = error.getMessages();
// ["Name is required", "Invalid email format", "Age must be positive"]

getErrorsForPath(path)

Get errors for a specific field:
const nameErrors = error.getErrorsForPath("name");
// [{ path: ["name"], message: "Name is required" }]

const addressCityErrors = error.getErrorsForPath("address.city");
// [{ path: ["address", "city"], message: "City is required" }]

hasErrorsForPath(path)

Check if a field has errors:
if (error.hasErrorsForPath("email")) {
  // Show email field error
}

toJSON()

Serialize for API responses:
const json = error.toJSON();
// {
//   name: "ValidationError",
//   message: "Validation failed: ...",
//   issues: [...]
// }

Static Methods

isValidationError(error)

Type-safe check that works across module boundaries:
try {
  // ...
} catch (error) {
  if (ValidationError.isValidationError(error)) {
    // error is typed as ValidationError
    console.log(error.issues);
  }
}
Always use ValidationError.isValidationError() instead of instanceof for reliable detection across module boundaries.

Throwing Validation Errors

In Hooks

Use the throwValidationError helper:
import { throwValidationError } from "@woltz/rich-domain";

rules: (entity) => {
  if (entity.price < 0) {
    throwValidationError("price", "Price cannot be negative");
  }
};

Manually

Create and throw directly:
import { ValidationError, createValidationIssue } from "@woltz/rich-domain";

// Single issue
throw new ValidationError([
  createValidationIssue("email", "Email already exists"),
]);

// Multiple issues
throw new ValidationError([
  createValidationIssue("email", "Email already exists"),
  createValidationIssue("username", "Username already taken"),
]);

// Nested path
throw new ValidationError([
  createValidationIssue(["address", "zipCode"], "Invalid ZIP code"),
]);

Non-Throwing Mode

Configure entities to collect errors instead of throwing:
class UserSafe extends Aggregate<UserProps> {
  protected static validation = {
    schema: userSchema,
    config: {
      onCreate: true,
      onUpdate: true,
      throwOnError: false, // Collect errors
    },
  };
}

const user = new UserSafe({ name: "", email: "invalid" });

// Check for errors
if (user.hasValidationErrors) {
  const errors = user.validationErrors!;

  console.log(errors.getMessages());
  // ["Name is required", "Invalid email format"]
}

When to Use Non-Throwing Mode

Form Validation

Collect all errors to display to user at once

Import/Migration

Log errors but continue processing

Partial Validation

Allow incomplete entities during construction

Error Aggregation

Combine errors from multiple sources

API Error Responses

Express.js Example

import express from "express";
import { ValidationError } from "@woltz/rich-domain";

const app = express();

// Error handling middleware
app.use(
  (
    err: Error,
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) => {
    if (ValidationError.isValidationError(err)) {
      return res.status(400).json({
        error: "Validation Error",
        message: err.message,
        details: err.issues.map((issue) => ({
          field: issue.path.join("."),
          message: issue.message,
        })),
      });
    }

    // Other errors
    console.error(err);
    res.status(500).json({ error: "Internal Server Error" });
  }
);

// Route
app.post("/users", async (req, res, next) => {
  try {
    const user = new User(req.body);
    await userRepository.save(user);
    res.status(201).json(user.toJSON());
  } catch (error) {
    next(error); // Pass to error handler
  }
});

Response Format

{
  "error": "Validation Error",
  "message": "Validation failed: Name is required, Invalid email format",
  "details": [
    { "field": "name", "message": "Name is required" },
    { "field": "email", "message": "Invalid email format" }
  ]
}

Domain Exceptions

Beyond ValidationError, rich-domain provides a comprehensive set of domain exceptions for different error scenarios. All exceptions extend a common DomainException base class.
import {
  DomainError,
  EntityNotFoundError,
  EntityAlreadyExistsError,
  RepositoryError,
  PersistenceError,
  ConcurrencyError,
  // ... more
} from "@woltz/rich-domain";

Exception Hierarchy

DomainException (base)
├── DomainError
├── EntityNotFoundError
├── EntityAlreadyExistsError
├── InvalidValueObjectError
├── InvalidCriteriaError
├── RepositoryError
│   ├── PersistenceError
│   ├── ConcurrencyError
│   └── ConstraintViolationError
├── DomainEventError
│   └── EventHandlerError
├── TransactionError
├── MapperError
├── ConfigurationError
├── NotImplementedError
└── UnknownError

Common Properties

All domain exceptions share these properties:
interface DomainException extends Error {
  name: string; // Exception class name
  message: string; // Error message
  code: string; // Error code (e.g., "ENTITY_NOT_FOUND")
  timestamp: Date; // When the error occurred

  toJSON(): object; // Serialize for API responses
}

// Type checking (works across module boundaries)
DomainException.isDomainException(error); // boolean

Entity & Aggregate Exceptions

DomainError

General-purpose exception for business rule violations:
import { DomainError } from "@woltz/rich-domain";

class Order extends Aggregate<OrderProps> {
  confirm() {
    if (this.items.length === 0) {
      throw new DomainError("Cannot confirm an empty order");
    }

    if (this.status !== "draft") {
      throw new DomainError(
        "Only draft orders can be confirmed",
        "ORDER_NOT_DRAFT" // Optional custom code
      );
    }

    this.props.status = "confirmed";
  }
}

EntityNotFoundError

When an entity or aggregate cannot be found:
import { EntityNotFoundError } from "@woltz/rich-domain";

class UserRepository {
  async findByIdOrFail(id: string): Promise<User> {
    const user = await this.findById(id);

    if (!user) {
      throw new EntityNotFoundError("User", id);
      // Message: "User with id 'abc-123' not found"
      // Code: "ENTITY_NOT_FOUND"
    }

    return user;
  }
}

// With custom message
throw new EntityNotFoundError(
  "User",
  id,
  "The requested user account does not exist"
);

EntityAlreadyExistsError

When trying to create an entity that already exists:
import { EntityAlreadyExistsError } from "@woltz/rich-domain";

class UserService {
  async createUser(email: string, name: string): Promise<User> {
    const existing = await this.userRepo.findByEmail(email);

    if (existing) {
      throw new EntityAlreadyExistsError("User", existing.id.value);
      // Message: "User with id 'abc-123' already exists"
    }

    return new User({ email, name });
  }
}

Repository & Persistence Exceptions

RepositoryError

Base exception for repository operations:
import { RepositoryError } from "@woltz/rich-domain";

throw new RepositoryError("Failed to connect to database");

PersistenceError

When a database operation fails:
import { PersistenceError } from "@woltz/rich-domain";

class UserRepository {
  async save(user: User): Promise<void> {
    try {
      await this.db.user.upsert({ ... });
    } catch (error) {
      throw new PersistenceError(
        "save",                    // Operation name
        "Database connection lost", // Message
        error as Error             // Original error (cause)
      );
    }
  }
}

// Access details
catch (error) {
  if (error instanceof PersistenceError) {
    console.log(error.operation); // "save"
    console.log(error.cause);     // Original database error
  }
}

ConcurrencyError

For optimistic locking conflicts:
import { ConcurrencyError } from "@woltz/rich-domain";

class OrderRepository {
  async save(order: Order): Promise<void> {
    const result = await this.db.order.updateMany({
      where: {
        id: order.id.value,
        version: order.version, // Optimistic lock
      },
      data: {
        ...orderData,
        version: { increment: 1 },
      },
    });

    if (result.count === 0) {
      throw new ConcurrencyError("Order", order.id.value);
      // Message: "Concurrency conflict detected for Order with id 'order-123'"
    }
  }
}

ConstraintViolationError

When a database constraint is violated:
import { ConstraintViolationError } from "@woltz/rich-domain";

class UserRepository {
  async save(user: User): Promise<void> {
    try {
      await this.db.user.create({ data: userData });
    } catch (error) {
      if (isPrismaUniqueConstraintError(error)) {
        throw new ConstraintViolationError(
          "users_email_unique",
          "A user with this email already exists"
        );
      }
      throw error;
    }
  }
}

Other Exceptions

InvalidValueObjectError

When a Value Object receives invalid data:
import { InvalidValueObjectError } from "@woltz/rich-domain";

class Email extends ValueObject<{ value: string }> {
  constructor(props: { value: string }) {
    if (!isValidEmail(props.value)) {
      throw new InvalidValueObjectError(
        "Email",
        "Invalid email format",
        props.value // The invalid value
      );
    }
    super(props);
  }
}

InvalidCriteriaError

When a Criteria query is invalid:
import { InvalidCriteriaError } from "@woltz/rich-domain";

// Thrown automatically by Criteria when:
// - Invalid operator for field type
// - Invalid quantifier value

// Example: manually throwing
if (!allowedFields.includes(field)) {
  throw new InvalidCriteriaError(
    `Field '${field}' is not allowed for filtering`,
    field
  );
}

TransactionError

When a transaction operation fails:
import { TransactionError } from "@woltz/rich-domain";

class UnitOfWork {
  async commit(): Promise<void> {
    try {
      await this.db.$transaction(this.operations);
    } catch (error) {
      throw new TransactionError(
        "commit",
        "Failed to commit transaction",
        error as Error
      );
    }
  }
}

MapperError

When mapping between domain and persistence fails:
import { MapperError } from "@woltz/rich-domain";

class UserToDomainMapper {
  build(record: UserRecord): User {
    try {
      return new User({
        id: Id.from(record.id),
        name: record.name,
        email: record.email,
      });
    } catch (error) {
      throw new MapperError(
        "toDomain",
        "User",
        "Failed to map user record to domain",
        error as Error
      );
    }
  }
}

DomainEventError & EventHandlerError

For event-related failures:
import { DomainEventError, EventHandlerError } from "@woltz/rich-domain";

// General event error
throw new DomainEventError("Failed to publish event", "UserCreatedEvent");

// Handler-specific error
throw new EventHandlerError(
  "SendWelcomeEmailHandler",
  "UserCreatedEvent",
  originalError
);

Utility Exceptions

import {
  NotImplementedError,
  ConfigurationError,
  UnknownError,
} from "@woltz/rich-domain";

// Feature not yet implemented
throw new NotImplementedError("Bulk import");

// Missing or invalid configuration
throw new ConfigurationError("Database URL is required", "DATABASE_URL");

// Wrap unknown errors
throw new UnknownError("An unexpected error occurred", originalError);

Handling Domain Exceptions

Centralized Error Handler

import {
  ValidationError,
  EntityNotFoundError,
  EntityAlreadyExistsError,
  ConcurrencyError,
  DomainError,
} from "@woltz/rich-domain";

function handleError(error: Error, res: Response) {
  // Validation errors → 400
  if (ValidationError.isValidationError(error)) {
    return res.status(400).json({
      error: "Validation Error",
      message: error.message,
      details: error.issues,
    });
  }

  // Not found → 404
  if (error instanceof EntityNotFoundError) {
    return res.status(404).json({
      error: "Not Found",
      message: error.message,
      entityType: error.entityType,
      entityId: error.entityId,
    });
  }

  // Already exists → 409
  if (error instanceof EntityAlreadyExistsError) {
    return res.status(409).json({
      error: "Conflict",
      message: error.message,
    });
  }

  // Concurrency conflict → 409
  if (error instanceof ConcurrencyError) {
    return res.status(409).json({
      error: "Conflict",
      message: "Resource was modified by another request",
      code: error.code,
    });
  }

  // Domain errors → 422
  if (error instanceof DomainError) {
    return res.status(422).json({
      error: "Unprocessable Entity",
      message: error.message,
      code: error.code,
    });
  }

  // Unknown errors → 500
  console.error("Unhandled error:", error);
  return res.status(500).json({
    error: "Internal Server Error",
    message: "An unexpected error occurred",
  });
}

Exception Reference Table

ExceptionHTTP StatusWhen to Use
ValidationError400Invalid input data
InvalidCriteriaError400Invalid query parameters
EntityNotFoundError404Resource not found
EntityAlreadyExistsError409Duplicate resource
ConcurrencyError409Optimistic lock conflict
ConstraintViolationError409Database constraint failed
DomainError422Business rule violation
InvalidValueObjectError422Invalid value object
PersistenceError500Database error
TransactionError500Transaction failed
MapperError500Mapping failed
ConfigurationError500Configuration missing