How to Handle Database Transactions in Rust

Use a transaction manager trait from your specific database driver (like `sqlx`, `diesel`, or `tokio-postgres`) to wrap database operations in a single atomic block, ensuring that either all changes commit or none do if an error occurs.

Use a transaction manager trait from your specific database driver (like sqlx, diesel, or tokio-postgres) to wrap database operations in a single atomic block, ensuring that either all changes commit or none do if an error occurs. You typically acquire a connection, start a transaction, execute your queries, and then explicitly commit or let the transaction abort if a panic or error propagates.

Here is a practical example using sqlx with PostgreSQL, which is the most common pattern for async Rust applications:

use sqlx::{PgPool, Error};

async fn transfer_funds(pool: &PgPool, from_id: i32, to_id: i32, amount: i64) -> Result<(), Error> {
    // Start a transaction
    let mut tx = pool.begin().await?;

    // Deduct from sender
    sqlx::query("UPDATE accounts SET balance = balance - $1 WHERE id = $2")
        .bind(amount)
        .bind(from_id)
        .execute(&mut *tx)
        .await?;

    // Add to receiver
    sqlx::query("UPDATE accounts SET balance = balance + $1 WHERE id = $2")
        .bind(amount)
        .bind(to_id)
        .execute(&mut *tx)
        .await?;

    // Commit the transaction
    tx.commit().await?;
    Ok(())
}

If any query fails or the function returns an Err before commit() is called, the transaction automatically rolls back. You can also explicitly roll back using tx.rollback().await? if you need to handle a specific error case before exiting.

For synchronous applications or when using diesel, the pattern is similar but relies on the TransactionManager trait. Here is how you might handle it with diesel:

use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, PooledConnection};

type DbConnection = PooledConnection<ConnectionManager<SqliteConnection>>;

fn update_records(conn: &mut DbConnection) -> Result<(), diesel::result::Error> {
    conn.transaction(|conn| {
        // All operations inside this closure are part of the transaction
        diesel::sql_query("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
            .execute(conn)?;
        
        diesel::sql_query("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
            .execute(conn)?;
        
        Ok(()) // Returning Ok commits; returning Err rolls back
    })
}

Key things to remember: always check the result of every query inside the transaction block, and avoid long-running operations that might hold locks for too long. If you are using async code, ensure you don't await on a transaction handle across a .await point if your driver doesn't support it, though modern drivers like sqlx handle this safely. If a panic occurs, the transaction will not commit, but you should still ensure your application logic handles the resulting rollback gracefully.