# Error Handling & Concurrency Documentation
This document covers error handling, concurrency, edge cases, and production considerations for ormkit.
## Table of Contents
1. [Error Categories](#error-categories)
2. [Error Handling](#error-handling)
3. [Concurrency Safety](#concurrency-safety)
4. [Edge Cases](#edge-cases)
5. [Performance Considerations](#performance-considerations)
6. [Production Best Practices](#production-best-practices)
---
## Error Categories
Ormkit provides a centralized error type with categorization for easier handling:
```rust
use ormkit::OrmkitError;
pub enum OrmkitError {
Database(sqlx::Error),
Migration(MigrationError),
Validation(SchemaValidationError),
Relation(LoadError),
Cache(String),
Repository(RepositoryError),
Transaction(TransactionError),
EntityNotFound(String),
MultipleEntitiesFound,
InvalidQuery(String),
LockError(String),
}
impl OrmkitError {
/// Check if error is retryable (transient)
pub fn is_retryable(&self) -> bool { /* ... */ }
/// Get error category for logging/metrics
pub fn category(&self) -> &'static str { /* ... */ }
}
```
### Error Category Reference
| **Database** | `sqlx::Error` | Sometimes | Connection loss, timeout | Retry with exponential backoff |
| **Transaction** | `TransactionError` | Yes | Serialization failure | Automatic retry (serializable) |
| **EntityNotFound** | `OrmkitError::EntityNotFound` | No | Invalid ID | Return 404 or default |
| **MultipleEntitiesFound** | `OrmkitError::MultipleEntitiesFound` | No | Ambiguous query | Refine query criteria |
| **Validation** | `SchemaValidationError` | No | Schema mismatch | Fix entity or run migrations |
| **Migration** | `MigrationError` | No | Invalid migration | Review and fix migration |
| **Relation** | `LoadError` | No | Missing relationship | Fix entity definition |
| **Cache** | `OrmkitError::Cache` | No | Cache corruption | Clear cache, retry |
| **InvalidQuery** | `OrmkitError::InvalidQuery` | No | Malformed query | Fix query construction |
| **LockError** | `OrmkitError::LockError` | Sometimes | Deadlock, lock timeout | Retry with backoff |
### Retry Logic
```rust
use ormkit::OrmkitError;
use tokio::time::{sleep, Duration};
async fn with_retry<F, T, E>(mut f: F, max_retries: u32) -> Result<T, E>
where
F: FnMut() -> Pin<Box<dyn Future<Output = Result<T, E>> + Send>>,
E: std::fmt::Debug,
{
let mut retries = 0;
loop {
match f().await {
Ok(result) => return Ok(result),
Err(e) if retries < max_retries && should_retry(&e) => {
retries += 1;
let delay = Duration::from_millis(100 * 2_u64.pow(retries));
sleep(delay).await;
}
Err(e) => return Err(e),
}
}
}
fn should_retry<E>(error: &E) -> bool
where
E: std::fmt::Debug,
{
// Check if error is retryable
if let Some(ormkit_error) = error.downcast_ref::<OrmkitError>() {
return ormkit_error.is_retryable();
}
false
}
```
---
## Error Handling
### Error Types
ormkit defines several error types for different operations:
#### 1. Repository Errors
```rust
use ormkit::RepositoryError;
pub enum RepositoryError {
/// Database error from SQLx
Database(sqlx::Error),
/// Entity not found
NotFound,
/// Multiple entities returned when one expected
MultipleResults,
/// Invalid query parameters
InvalidQuery(String),
}
```
**Handling Repository Errors:**
```rust
use ormkit::{Repository, RepositoryError};
async fn handle_user(repo: &Repository<User>, id: Uuid) -> Result<User, Box<dyn std::error::Error>> {
match repo.find_by_id(id).await {
Ok(user) => Ok(user),
Err(RepositoryError::NotFound) => {
eprintln!("User {} not found", id);
Err(Box::new(RepositoryError::NotFound))
}
Err(RepositoryError::Database(e)) => {
eprintln!("Database error: {}", e);
Err(Box::new(e))
}
Err(e) => Err(Box::new(e)),
}
}
```
#### 2. Migration Errors
```rust
use ormkit::MigrationError;
pub enum MigrationError {
/// Database error
Database(sqlx::Error),
/// IO error (reading migration files)
Io(std::io::Error),
/// Migration already applied
AlreadyApplied { version: i64, name: String },
/// Migration not found
NotFound { version: i64, name: String },
/// Checksum mismatch
ChecksumMismatch { version: i64, name: String },
/// Migration execution failed
MigrationFailed { version: i64, message: String },
}
```
**Handling Migration Errors:**
```rust
use ormkit::{Migrator, MigrationError, MigrationConfig};
async fn run_migrations_safe(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
let config = MigrationConfig::new("./migrations");
let migrator = Migrator::new(pool, config).await?;
match migrator.up().await {
Ok(result) => {
println!("Applied {} migrations", result.applied_count);
Ok(())
}
Err(MigrationError::AlreadyApplied { version, name }) => {
eprintln!("Migration {} ({}) already applied", version, name);
// Not necessarily fatal - continue
Ok(())
}
Err(MigrationError::MigrationFailed { version, message }) => {
eprintln!("Migration {} failed: {}", version, message);
// Fatal - database might be in inconsistent state
Err("Migration failed".into())
}
Err(e) => Err(Box::new(e)),
}
}
```
#### 3. Validation Errors
```rust
use ormkit::SchemaValidationError;
pub enum SchemaValidationError {
/// Database error
Database(sqlx::Error),
/// Table not found in database
TableNotFound { table: String },
/// Column not found
ColumnNotFound { table: String, column: String },
/// Type mismatch
TypeMismatch {
table: String,
column: String,
expected: String,
found: String,
},
}
```
**Handling Validation Errors:**
```rust
use ormkit::SchemaValidator;
async fn validate_schema_safe(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
let validator = SchemaValidator::new(pool);
match validator.get_table_info("users").await {
Ok(table_info) => {
println!("Table '{}' validated successfully", table_info.name);
Ok(())
}
Err(SchemaValidationError::TableNotFound { table }) => {
eprintln!("Table '{}' not found - run migrations first", table);
Err("Schema validation failed".into())
}
Err(SchemaValidationError::ColumnNotFound { table, column }) => {
eprintln!("Column '{}' not found in table '{}'", column, table);
Err("Schema validation failed".into())
}
Err(e) => Err(Box::new(e)),
}
}
```
#### 4. Transaction Errors
```rust
use ormkit::TransactionError;
pub enum TransactionError {
/// Database error
Database(sqlx::Error),
/// Transaction already committed
AlreadyCommitted,
/// Transaction already rolled back
AlreadyRolledBack,
/// Transaction failed
TransactionFailed(String),
}
```
**Handling Transaction Errors:**
```rust
use ormkit::{transaction, TransactionError};
async fn transfer_money(
pool: &PgPool,
from: Uuid,
to: Uuid,
amount: i64,
) -> Result<(), Box<dyn std::error::Error>> {
transaction(pool, |tx| async move {
// Perform operations
// ...
Ok::<(), Box<dyn std::error::Error>>(())
}).await
.map_err(|e| {
match e {
TransactionError::Database(db_err) => {
eprintln!("Database error in transaction: {}", db_err);
Box::new(db_err) as Box<dyn std::error::Error>
}
TransactionError::TransactionFailed(msg) => {
eprintln!("Transaction failed: {}", msg);
Box::new(TransactionError::TransactionFailed(msg))
}
_ => Box::new(e)
}
})?;
Ok(())
}
```
### Error Propagation Best Practices
#### 1. Use Custom Error Types
```rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("User not found: {0}")]
UserNotFound(Uuid),
#[error("Validation error: {0}")]
Validation(String),
#[error("Permission denied")]
PermissionDenied,
}
```
#### 2. Provide Context
```rust
async fn get_user_with_posts(
pool: &PgPool,
user_id: Uuid,
) -> Result<UserWithPosts, AppError> {
let repo = Repository::new(pool);
let user = repo.find_by_id(user_id).await
.map_err(|e| {
AppError::Database(e)
})?
.ok_or(AppError::UserNotFound(user_id))?;
// Load posts...
Ok(UserWithPosts { user, posts: vec![] })
}
```
#### 3. Log Errors Appropriately
```rust
use tracing::{error, warn, info};
async fn handle_request(pool: &PgPool, user_id: Uuid) -> Result<(), AppError> {
match get_user(pool, user_id).await {
Ok(user) => {
info!("Successfully retrieved user: {}", user.id);
Ok(())
}
Err(AppError::UserNotFound(id)) => {
warn!("User not found: {}", id);
Err(AppError::UserNotFound(id))
}
Err(AppError::Database(e)) => {
error!("Database error while fetching user {}: {:?}", user_id, e);
Err(AppError::Database(e))
}
Err(e) => {
error!("Unexpected error: {:?}", e);
Err(e)
}
}
}
```
---
## Concurrency Safety
### Thread Safety
#### Repository Clone Pattern
```rust
use ormkit::Repository;
// Repositories are cheap to clone - they just hold an Arc to the pool
fn create_repo(pool: &PgPool) -> Repository<User> {
Repository::new(pool)
}
// Safe to share across threads
async fn concurrent_queries(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
let repo = Repository::new(pool);
let task1 = {
let repo = repo.clone();
tokio::spawn(async move {
repo.find_by_id(Uuid::new_v4()).await
})
};
let task2 = {
let repo = repo.clone();
tokio::spawn(async move {
repo.find_all().await
})
};
let (result1, result2) = tokio::join!(task1, task2);
Ok(())
}
```
#### Connection Pool Considerations
```rust
// PgPool is thread-safe and can be shared across tasks
async fn share_pool() -> Result<(), Box<dyn std::error::Error>> {
let pool = PgPool::connect("postgresql://...").await?;
// Create multiple repos from the same pool
let user_repo = Repository::<User>::new(&pool);
let post_repo = Repository::<Post>::new(&pool);
// Both can be used concurrently
let (users, posts) = tokio::join!(
user_repo.find_all(),
post_repo.find_all()
);
Ok(())
}
```
### Transaction Isolation
```rust
use ormkit::{transaction, IsolationLevel};
async fn isolated_transaction(
pool: &PgPool,
) -> Result<(), Box<dyn std::error::Error>> {
transaction_with_isolation(
pool,
IsolationLevel::Serializable,
|tx| async move {
// Serializable isolation prevents concurrent modifications
let repo = Repository::new(&tx);
// ...
Ok(())
}
).await?;
Ok(())
}
```
### Race Conditions
#### Avoiding Lost Updates
```rust
use ormkit::{Repository, ActiveModelTrait, active_value::ActiveValue};
// BAD: Lost update possible
async fn increment_bad(pool: &PgPool, user_id: Uuid) -> Result<(), Box<dyn std::error::Error>> {
let repo = Repository::new(pool);
let mut user = repo.find_by_id(user_id).await?.unwrap();
user.score += 1; // Race condition here!
repo.update(user.into_active_model()).await?;
Ok(())
}
// GOOD: Use database atomic operations
async fn increment_good(pool: &PgPool, user_id: Uuid) -> Result<(), Box<dyn std::error::Error>> {
sqlx::query(
"UPDATE users SET score = score + 1 WHERE id = $1"
)
.bind(user_id)
.execute(pool)
.await?;
Ok(())
}
```
#### Optimistic Locking
```rust
#[derive(ormkit::Entity)]
#[ormkit(table = "products")]
struct Product {
#[ormkit(id)]
id: Uuid,
name: String,
version: i32, // For optimistic locking
}
async fn update_product_safe(
pool: &PgPool,
mut product: Product,
) -> Result<(), Box<dyn std::error::Error>> {
let repo = Repository::new(pool);
// Use version in WHERE clause
let result = sqlx::query(
"UPDATE products SET name = $1, version = version + 1 \
WHERE id = $2 AND version = $3"
)
.bind(&product.name)
.bind(product.id)
.bind(product.version)
.execute(pool)
.await?;
if result.rows_affected() == 0 {
return Err("Product was modified by another transaction".into());
}
Ok(())
}
```
### Deadlock Prevention
```rust
use ormkit::transaction;
// BAD: Potential deadlock if transactions acquire locks in different order
async fn transfer_bad(
pool: &PgPool,
account_a: Uuid,
account_b: Uuid,
amount: i64,
) -> Result<(), Box<dyn std::error::Error>> {
transaction(pool, |tx| async move {
// Lock account_a first
sqlx::query("SELECT * FROM accounts WHERE id = $1 FOR UPDATE")
.bind(account_a)
.fetch_one(&tx)
.await?;
// Then lock account_b
sqlx::query("SELECT * FROM accounts WHERE id = $1 FOR UPDATE")
.bind(account_b)
.fetch_one(&tx)
.await?;
// Transfer...
Ok(())
}).await?;
Ok(())
}
// GOOD: Always acquire locks in a consistent order
async fn transfer_good(
pool: &PgPool,
account_a: Uuid,
account_b: Uuid,
amount: i64,
) -> Result<(), Box<dyn std::error::Error>> {
// Sort IDs to ensure consistent locking order
let (first, second) = if account_a < account_b {
(account_a, account_b)
} else {
(account_b, account_a)
};
transaction(pool, move |tx| async move {
// Always lock the smaller ID first
sqlx::query("SELECT * FROM accounts WHERE id = $1 FOR UPDATE")
.bind(first)
.fetch_one(&tx)
.await?;
sqlx::query("SELECT * FROM accounts WHERE id = $1 FOR UPDATE")
.bind(second)
.fetch_one(&tx)
.await?;
// Transfer...
Ok(())
}).await?;
Ok(())
}
```
---
## Edge Cases
### NULL Handling
```rust
// Nullable columns should use Option<T>
#[derive(ormkit::Entity)]
#[ormkit(table = "users")]
struct User {
#[ormkit(id)]
id: Uuid,
name: String,
bio: Option<String>, // Nullable
}
// Querying NULL values
async fn find_users_without_bio(pool: &PgPool) -> Result<Vec<User>, Box<dyn std::error::Error>> {
let users = sqlx::query_as::<_, User>(
"SELECT * FROM users WHERE bio IS NULL"
)
.fetch_all(pool)
.await?;
Ok(users)
}
// Using query builder with NULL filters
async fn find_users_with_null_filter(pool: &PgPool) -> Result<Vec<User>, Box<dyn std::error::Error>> {
use ormkit::{Entity, FilterOp};
let users = User::query()
.filter_null("bio", true) // WHERE bio IS NULL
.fetch_all(pool)
.await?;
Ok(users)
}
```
### Empty Results
```rust
use ormkit::Repository;
async fn handle_user_safe(pool: &PgPool, id: Uuid) -> Result<Option<User>, sqlx::Error> {
let repo = Repository::new(pool);
// find_by_id returns Option<User>, so empty results are not errors
let user = repo.find_by_id(id).await?;
Ok(user)
}
// Proper error handling
async fn get_user_or_default(pool: &PgPool, id: Uuid) -> User {
let repo = Repository::new(pool);
match repo.find_by_id(id).await {
Ok(Some(user)) => user,
Ok(None) => {
eprintln!("User {} not found, using default", id);
User::default()
}
Err(e) => {
eprintln!("Error fetching user: {}", e);
User::default()
}
}
}
```
### Large Datasets
```rust
use ormkit::pagination::{PaginationRequest, Paginator};
// BAD: Loading everything into memory
async fn load_all_bad(pool: &PgPool) -> Result<Vec<User>, Box<dyn std::error::Error>> {
let repo = Repository::new(pool);
let all_users = repo.find_all().await?; // Could be millions!
Ok(all_users)
}
// GOOD: Use pagination
async fn load_all_good(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
let repo = Repository::new(pool);
let mut page = 1;
loop {
let request = PaginationRequest::new(page, 1000);
let result = repo.paginate(&request).await?;
if result.items.is_empty() {
break;
}
// Process page
for user in result.items {
// Process user
}
page += 1;
}
Ok(())
}
// EVEN BETTER: Use streaming
async fn stream_users(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
let mut stream = sqlx::query_as::<_, User>("SELECT * FROM users")
.fetch(pool);
while let Some(user) = stream.try_next().await? {
// Process user one at a time
}
Ok(())
}
```
### Connection Pool Exhaustion
```rust
use sqlx::PgPoolOptions;
// BAD: Default pool settings
async fn create_pool_bad() -> Result<PgPool, sqlx::Error> {
PgPool::connect("postgresql://...").await
}
// GOOD: Tuned pool settings
async fn create_pool_good() -> Result<PgPool, sqlx::Error> {
PgPoolOptions::new()
.max_connections(20) // Increase from default 5
.min_connections(5) // Keep some connections ready
.acquire_timeout(std::time::Duration::from_secs(30))
.idle_timeout(std::time::Duration::from_secs(600))
.max_lifetime(std::time::Duration::from_secs(1800))
.connect("postgresql://...")
.await
}
// Handle pool exhaustion gracefully
async fn query_with_timeout(pool: &PgPool) -> Result<User, Box<dyn std::error::Error>> {
let repo = Repository::new(pool);
tokio::time::timeout(
std::time::Duration::from_secs(5),
repo.find_by_id(Uuid::new_v4())
)
.await
.map_err(|_| "Query timeout")???;
Ok(())
}
```
### Timezone Handling
```rust
use chrono::{DateTime, Utc};
// Always use TIMESTAMPTZ (timestamp with timezone) in PostgreSQL
#[derive(ormkit::Entity)]
#[ormkit(table = "events")]
struct Event {
#[ormkit(id)]
id: Uuid,
name: String,
// Use DateTime<Utc> for TIMESTAMPTZ columns
created_at: DateTime<Utc>,
occurred_at: DateTime<Utc>,
}
// Converting timezones
async fn create_event_with_local_time(
pool: &PgPool,
name: String,
local_time: DateTime<chrono::Local>,
) -> Result<(), Box<dyn std::error::Error>> {
let event = Event {
id: Uuid::new_v4(),
name,
created_at: Utc::now(),
// Convert local time to UTC before storing
occurred_at: local_time.with_timezone(&Utc),
};
let repo = Repository::new(pool);
repo.insert(event.into_active_model()).await?;
Ok(())
}
```
---
## Performance Considerations
### N+1 Query Prevention
```rust
// BAD: N+1 queries
async fn load_posts_with_users_bad(pool: &PgPool) -> Result<Vec<PostWithUser>, Box<dyn std::error::Error>> {
let posts = sqlx::query_as::<_, Post>("SELECT * FROM posts")
.fetch_all(pool)
.await?;
let mut result = Vec::new();
for post in posts {
// N+1: Separate query for each post's user
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(post.user_id)
.fetch_one(pool)
.await?;
result.push(PostWithUser { post, user });
}
Ok(result)
}
// GOOD: Eager loading
async fn load_posts_with_users_good(pool: &PgPool) -> Result<Vec<PostWithUser>, Box<dyn std::error::Error>> {
let posts = sqlx::query_as::<_, Post>("SELECT * FROM posts")
.fetch_all(pool)
.await?;
// Collect unique user IDs
let user_ids: Vec<Uuid> = posts.iter().map(|p| p.user_id).collect();
// Load all users in ONE query
let users = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ANY($1)")
.bind(&user_ids)
.fetch_all(pool)
.await?;
// Create lookup map
let user_map: std::collections::HashMap<Uuid, User> =
users.into_iter().map(|u| (u.id, u)).collect();
// Match posts to users
let result: Vec<PostWithUser> = posts.into_iter()
.map(|post| {
let user = user_map.get(&post.user_id).cloned().unwrap();
PostWithUser { post, user }
})
.collect();
Ok(result)
}
// EVEN BETTER: Use ormkit's RelationBuilder
async fn load_posts_with_users_best(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
let posts = Post::query().fetch_all(pool).await?;
let posts_with_users = RelationBuilder::for_entities(posts)
.load::<User>(pool)
.await?;
for loaded in posts_with_users {
println!("{} by {}", loaded.entity.title, loaded.related.name);
}
Ok(())
}
```
### Index Usage
```sql
-- Create indexes for commonly queried columns
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
-- Composite index for queries with multiple conditions
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC);
-- Partial index for specific queries
CREATE INDEX idx_users_active ON users(email) WHERE active = true;
```
### Query Optimization
```rust
// Use SELECT only the columns you need
async fn get_user_names(pool: &PgPool) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let names = sqlx::query_scalar::<_, String>(
"SELECT name FROM users WHERE active = true"
)
.fetch_all(pool)
.await?;
Ok(names)
}
// Use EXISTS instead of COUNT for checking existence
async fn user_exists(pool: &PgPool, email: &str) -> Result<bool, Box<dyn std::error::Error>> {
let exists = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)"
)
.bind(email)
.fetch_one(pool)
.await?;
Ok(exists)
}
// Use batch operations
use ormkit::batch::{insert_many, BatchOptions};
async fn bulk_insert(users: Vec<User>) -> Result<(), Box<dyn std::error::Error>> {
let options = BatchOptions::new()
.batch_size(100);
insert_many(&pool, &users, Some(options)).await?;
Ok(())
}
```
---
## Production Best Practices
### 1. Always Use Transactions for Multi-Step Operations
```rust
use ormkit::transaction;
async fn create_user_with_posts(
pool: &PgPool,
user_name: String,
post_titles: Vec<String>,
) -> Result<(), Box<dyn std::error::Error>> {
transaction(pool, |tx| async move {
let repo = Repository::new(&tx);
// Create user
let mut user = User::active_model();
user.name = ActiveValue::Set(user_name);
let user = repo.insert(user).await?;
// Create posts
for title in post_titles {
let mut post = Post::active_model();
post.title = ActiveValue::Set(title);
post.user_id = ActiveValue::Set(user.id);
repo.insert(post).await?;
}
Ok::<(), Box<dyn std::error::Error>>(())
}).await?;
Ok(())
}
```
### 2. Implement Retry Logic for Transient Failures
```rust
async fn query_with_retry<F, T>(
mut operation: F,
max_retries: u32,
) -> Result<T, Box<dyn std::error::Error>>
where
F: FnMut() -> Pin<Box<dyn Future<Output = Result<T, Box<dyn std::error::Error>>> + Send>>,
{
let mut attempt = 0;
loop {
match operation().await {
Ok(result) => return Ok(result),
Err(e) if attempt < max_retries => {
eprintln!("Attempt {} failed: {}, retrying...", attempt + 1, e);
attempt += 1;
tokio::time::sleep(std::time::Duration::from_millis(100 * attempt as u64)).await;
}
Err(e) => return Err(e),
}
}
}
```
### 3. Use Prepared Statements
```rust
// ormkit uses prepared statements automatically via SQLx
// No extra work needed - just use the query builder
async fn find_by_email(pool: &PgPool, email: &str) -> Result<Option<User>, sqlx::Error> {
// This uses a prepared statement
let user = sqlx::query_as::<_, User>(
"SELECT * FROM users WHERE email = $1"
)
.bind(email)
.fetch_optional(pool)
.await?;
Ok(user)
}
```
### 4. Monitor Connection Pool Health
```rust
async fn monitor_pool(pool: &PgPool) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
loop {
interval.tick().await;
let state = pool.state();
println!("Pool state:");
println!(" Total connections: {}", state.connections);
println!(" Idle connections: {}", state.idle_connections);
}
}
```
### 5. Use Appropriate Isolation Levels
```rust
use ormkit::{transaction_with_isolation, IsolationLevel};
// ReadCommitted: Good for most operations
async fn standard_operation(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
transaction_with_isolation(pool, IsolationLevel::ReadCommitted, |tx| async move {
// ...
Ok(())
}).await?;
Ok(())
}
// Serializable: For critical operations requiring absolute consistency
async fn financial_transaction(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
transaction_with_isolation(pool, IsolationLevel::Serializable, |tx| async move {
// ...
Ok(())
}).await?;
Ok(())
}
```
---
This concludes the error handling and concurrency documentation. For more information, see:
- [PRODUCTION_FEATURES.md](PRODUCTION_FEATURES.md) - Migration and validation features
- [README.md](README.md) - Main documentation