Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Types

EsRepo generates four per-entity error types and a column enum. For an entity called User, the macro produces:

TypeUsed by
UserColumnEnum of indexed columns (e.g. Id, Name, Email)
UserCreateErrorcreate, create_all
UserModifyErrorupdate, update_all, delete
UserFindErrorfind_by_*, maybe_find_by_*
UserQueryErrorfind_all, list_by_*, list_for_*, list_for_filters

UserCreateError

pub enum UserCreateError {
    Sqlx(sqlx::Error),
    ConstraintViolation {
        column: Option<UserColumn>,
        value: Option<String>,
        inner: sqlx::Error,
    },
    ConcurrentModification,
    HydrationError(EntityHydrationError),
    PostPersistHookError(/* only if post_persist_hook configured */),
    PostHydrateError(/* only if post_hydrate_hook configured */),
}

PostPersistHookError and PostHydrateError are only present when the corresponding hook is configured. PostPersistHookError wraps sqlx::Error by default, or a custom error type if configured via post_persist_hook(error = "..."). See Hooks for details.

Handling constraint violations

When a create or create_all operation violates a unique constraint, the error is returned as ConstraintViolation rather than a raw Sqlx error. The column field identifies which column caused the violation and the value field contains the conflicting value extracted from the PostgreSQL error detail.

let result = users.create(new_user).await;
match result {
    Ok(user) => { /* success */ }
    // Column-agnostic check
    Err(e) if e.was_duplicate() => {
        println!("some unique constraint violated");
    }
    Err(e) => return Err(e.into()),
}

// Or check a specific column:
match result {
    Ok(user) => { /* success */ }
    Err(e) if e.was_duplicate_by(UserColumn::Email) => {
        let value = e.duplicate_value(); // Option<&str>
        println!("email {} already taken", value.unwrap_or("unknown"));
    }
    Err(e) => return Err(e.into()),
}

The macro maps PostgreSQL constraint names to columns automatically using the convention {table}_{column}_key for unique constraints and {table}_pkey for the primary key. If your constraint uses a non-standard name, specify it explicitly:

#[derive(EsRepo)]
#[es_repo(
    entity = "User",
    columns(
        email(ty = "String", constraint = "idx_unique_email"),
    )
)]
pub struct Users {
    pool: sqlx::PgPool,
}

Concurrent modification

When optimistic concurrency control detects a conflict (duplicate event sequence), the error is ConcurrentModification:

if e.was_concurrent_modification() {
    // retry the operation
}

UserModifyError

UserModifyError has the same structure as UserCreateError (minus HydrationError and PostHydrateError) and is returned by update, update_all, and delete. PostPersistHookError is only present when post_persist_hook is configured. It provides the same was_duplicate, was_duplicate_by, duplicate_value, and was_concurrent_modification helpers.

Nested entity errors

For aggregates with nested entities (e.g. Order containing OrderItems), CreateError and ModifyError include additional variants wrapping the child’s errors. The duplicate_value and was_concurrent_modification helpers cascade into nested errors automatically:

// If a nested OrderItem creation triggers a constraint violation,
// duplicate_value() still returns the conflicting value:
let val = err.duplicate_value(); // cascades into nested variants

The was_duplicate_by helper does not cascade because nested entities have a different column enum. To check which nested column was violated, match the nested variant directly:

match err {
    OrderModifyError::OrderItemsCreate(item_err)
        if item_err.was_duplicate_by(OrderItemColumn::Sku) =>
    {
        let val = item_err.duplicate_value();
    }
    _ => return Err(err.into()),
}

UserFindError

pub enum UserFindError {
    Sqlx(sqlx::Error),
    NotFound { entity: &'static str, column: Option<UserColumn>, value: String },
    HydrationError(EntityHydrationError),
    PostHydrateError(/* only if post_hydrate_hook configured */),
}

The NotFound variant is returned by find_by_* methods when no matching row exists. It includes the entity name, the column searched (as the UserColumn enum), and the value that was not found.

Checking for not-found

let result = users.find_by_id(some_id).await;
match result {
    Ok(user) => { /* found */ }
    Err(e) if e.was_not_found() => {
        println!("user not found");
    }
    Err(e) => return Err(e.into()),
}

Matching on a specific column

Use was_not_found_by to check which column was searched, or pattern-match directly on the NotFound variant for full control:

// Helper method
if e.was_not_found_by(UserColumn::Email) {
    let value = e.not_found_value(); // Option<&str>
    println!("no user with email {}", value.unwrap_or("unknown"));
}

// Pattern matching for custom error conversion
impl From<UserFindError> for AppError {
    fn from(error: UserFindError) -> Self {
        match error {
            UserFindError::NotFound {
                column: Some(UserColumn::Id),
                value,
                ..
            } => Self::UserNotFoundById(value),
            UserFindError::NotFound {
                column: Some(UserColumn::Email),
                value,
                ..
            } => Self::UserNotFoundByEmail(value),
            other => Self::Internal(other.into()),
        }
    }
}

Use maybe_find_by_* to get Ok(None) instead of an error when the entity doesn’t exist.

UserQueryError

pub enum UserQueryError {
    Sqlx(sqlx::Error),
    HydrationError(EntityHydrationError),
    CursorDestructureError(CursorDestructureError),
    PostHydrateError(/* only if post_hydrate_hook configured */),
}

Returned by paginated list operations (list_by_*, list_for_*, list_for_filters). The CursorDestructureError variant occurs when a pagination cursor cannot be decoded.