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 Handling

RouchDB uses a single error enum, RouchError, for all error conditions across every crate in the workspace. All fallible functions return Result<T>, which is an alias for std::result::Result<T, RouchError>.

#![allow(unused)]
fn main() {
use rouchdb::{Result, RouchError};
}

The Result Type

#![allow(unused)]
fn main() {
pub type Result<T> = std::result::Result<T, RouchError>;
}

Every async method on Database and every Adapter trait method returns Result<T>. This makes error handling consistent throughout the entire API.


RouchError Variants

#![allow(unused)]
fn main() {
#[derive(Debug, Error)]
pub enum RouchError {
    NotFound(String),
    Conflict,
    BadRequest(String),
    Unauthorized,
    Forbidden(String),
    InvalidRev(String),
    MissingId,
    DatabaseExists(String),
    DatabaseError(String),
    Io(#[from] std::io::Error),
    Json(#[from] serde_json::Error),
}
}

Variant Reference

VariantDisplay FormatWhen It Occurs
NotFound(String)"not found: {0}"A document, revision, attachment, or local document does not exist. The string contains the ID or a descriptive message.
Conflict"conflict: document update conflict"An update or delete was attempted without the correct current _rev. The document has been modified since the revision you provided.
BadRequest(String)"bad request: {0}"The request is malformed. Examples: document body is not a JSON object, invalid query parameters, or invalid selector syntax.
Unauthorized"unauthorized"Authentication is required but not provided. Returned by the HTTP adapter when CouchDB responds with 401.
Forbidden(String)"forbidden: {0}"The authenticated user does not have permission for this operation. Returned by the HTTP adapter when CouchDB responds with 403.
InvalidRev(String)"invalid revision format: {0}"A revision string could not be parsed. Revisions must be in {pos}-{hash} format where pos is a positive integer (e.g., "3-abc123").
MissingId"missing document id"A document write was attempted without a document ID.
DatabaseExists(String)"database already exists: {0}"An attempt was made to create a database that already exists.
DatabaseError(String)"database error: {0}"A general database-level error (storage corruption, adapter failure, unexpected internal state).
Io(std::io::Error)"io error: {0}"An I/O error from the underlying storage layer (file system, network). Automatically converted from std::io::Error via #[from].
Json(serde_json::Error)"json error: {0}"A JSON serialization or deserialization error. Automatically converted from serde_json::Error via #[from].

Matching on Specific Errors

Use Rust’s match expression to handle different error conditions:

#![allow(unused)]
fn main() {
use rouchdb::{Database, RouchError};

async fn handle_get(db: &Database, id: &str) {
    match db.get(id).await {
        Ok(doc) => {
            println!("Found: {}", doc.data);
        }
        Err(RouchError::NotFound(_)) => {
            println!("Document {} does not exist", id);
        }
        Err(RouchError::Unauthorized) => {
            eprintln!("Authentication required");
        }
        Err(e) => {
            eprintln!("Unexpected error: {}", e);
        }
    }
}
}

Common Error Patterns

Conflict Resolution

The most common error in document databases is the update conflict. It occurs when you try to update a document but someone else has modified it since you last read it.

#![allow(unused)]
fn main() {
use rouchdb::{Database, RouchError};

async fn safe_update(db: &Database, id: &str) -> rouchdb::Result<()> {
    loop {
        // Read the current version
        let doc = db.get(id).await?;
        let rev = doc.rev.as_ref().unwrap().to_string();

        // Modify the data
        let mut data = doc.data.clone();
        data["counter"] = json!(data["counter"].as_i64().unwrap_or(0) + 1);

        // Attempt the update
        match db.update(id, &rev, data).await {
            Ok(result) => {
                println!("Updated to rev {}", result.rev.unwrap());
                return Ok(());
            }
            Err(RouchError::Conflict) => {
                // Someone else updated the doc -- retry with the new version
                println!("Conflict detected, retrying...");
                continue;
            }
            Err(e) => return Err(e),
        }
    }
}
}

Create-if-not-exists

#![allow(unused)]
fn main() {
use rouchdb::{Database, RouchError};

async fn ensure_doc(db: &Database, id: &str) -> rouchdb::Result<()> {
    match db.get(id).await {
        Ok(_) => {
            // Document already exists, nothing to do
            Ok(())
        }
        Err(RouchError::NotFound(_)) => {
            // Document does not exist, create it
            db.put(id, json!({"created_at": "2026-02-07"})).await?;
            Ok(())
        }
        Err(e) => Err(e),
    }
}
}

Handling Bulk Write Results

bulk_docs does not return an error for individual document failures. Instead, check each DocResult:

#![allow(unused)]
fn main() {
let results = db.bulk_docs(docs, BulkDocsOptions::new()).await?;

for result in &results {
    if result.ok {
        println!("Wrote {} at rev {}", result.id, result.rev.as_deref().unwrap_or("?"));
    } else {
        eprintln!(
            "Failed to write {}: {} - {}",
            result.id,
            result.error.as_deref().unwrap_or("unknown"),
            result.reason.as_deref().unwrap_or("no reason"),
        );
    }
}
}

Using the ? Operator

Since RouchError implements std::error::Error (via thiserror), it works seamlessly with the ? operator and with error types from other crates:

#![allow(unused)]
fn main() {
async fn process(db: &Database) -> rouchdb::Result<()> {
    let doc = db.get("config").await?;         // RouchError on failure
    let info = db.info().await?;               // RouchError on failure
    db.put("status", json!({"ok": true})).await?; // RouchError on failure
    Ok(())
}
}

Converting from External Errors

RouchError has automatic From implementations for common external error types:

Source TypeConverts To
std::io::ErrorRouchError::Io
serde_json::ErrorRouchError::Json

This means I/O and JSON errors from third-party code are automatically converted when using ?:

#![allow(unused)]
fn main() {
async fn read_and_store(db: &Database, path: &str) -> rouchdb::Result<()> {
    let content = std::fs::read_to_string(path)?;  // io::Error -> RouchError::Io
    let value: serde_json::Value = serde_json::from_str(&content)?;  // -> RouchError::Json
    db.put("imported", value).await?;
    Ok(())
}
}

Replication Error Handling

Replication errors are handled differently. The replicate function returns a ReplicationResult rather than failing outright for individual document errors:

#![allow(unused)]
fn main() {
let result = local.replicate_to(&remote).await?;

if result.ok {
    println!("Replication complete: {} docs written", result.docs_written);
} else {
    eprintln!("Replication completed with errors:");
    for err in &result.errors {
        eprintln!("  - {}", err);
    }
}
}

The top-level Result only returns Err for catastrophic failures (network down, source database unreachable). Individual document write failures are collected in result.errors, and result.ok is false if any errors occurred.


Display and Debug

RouchError implements both Display (for user-facing messages) and Debug (for developer diagnostics):

#![allow(unused)]
fn main() {
let err = RouchError::NotFound("doc123".into());

// Display: "not found: doc123"
println!("{}", err);

// Debug: NotFound("doc123")
println!("{:?}", err);
}

All variants produce clear, actionable error messages suitable for logging.