Errors Reference (IC)
Note: This is the IC-specific error handling reference. For the complete error hierarchy, all error variants, and their causes, see the generic errors reference.
Overview
When using ic-dbms through the ic-dbms-client crate, error handling has an additional layer compared to direct wasm-dbms usage. The IC’s inter-canister call model introduces network-level errors alongside database-level errors, resulting in the double Result pattern.
IcDbmsError Type Alias
IcDbmsError is a re-export of DbmsError from wasm-dbms-api, provided by ic-dbms-api for convenience:
use ic_dbms_api::prelude::IcDbmsError;
// IcDbmsError is the same as wasm_dbms_api::DbmsError
// It provides the full error hierarchy:
pub enum IcDbmsError {
Memory(MemoryError),
Query(QueryError),
Table(TableError),
Transaction(TransactionError),
Sanitize(String),
Validation(String),
}
You can use IcDbmsError or DbmsError interchangeably. The IcDbmsError alias is conventional in IC codebases.
Double Result Pattern
Why Two Results?
Client operations return Result<Result<T, IcDbmsError>, CallError>:
Result< -- Outer: IC call result
Result<T, IcDbmsError>, -- Inner: Database operation result
CallError -- Network/canister call error
>
- Outer
Result(CallError): The inter-canister call itself failed. This happens when:- The canister is unreachable or stopped
- The canister ran out of cycles
- The message was rejected (e.g., unauthorized caller)
- Network timeout on agent calls
- Inner
Result(IcDbmsError): The call succeeded but the database operation failed. This happens when:- Primary key conflict
- Foreign key constraint violation
- Validation failure
- Transaction not found
- Any other database logic error
Using the ?? Operator
The simplest approach is to use ?? to unwrap both layers:
// Propagates both CallError and IcDbmsError
let users = client.select::<User>(User::table_name(), query, None).await??;
This requires your function to return an error type that both CallError and IcDbmsError can convert into (e.g., Box<dyn std::error::Error>, anyhow::Error, or a custom enum).
Explicit Error Handling
match client.insert::<User>(User::table_name(), user, None).await {
Ok(Ok(())) => {
// Success: call succeeded AND database operation succeeded
println!("Insert successful");
}
Ok(Err(db_error)) => {
// Call succeeded but database operation failed
println!("Database error: {:?}", db_error);
}
Err(call_error) => {
// Inter-canister call itself failed
println!("Call failed: {:?}", call_error);
}
}
Client Error Handling Examples
Basic Pattern
use ic_dbms_api::prelude::{IcDbmsError, QueryError};
let result = client.insert::<User>(User::table_name(), user, None).await;
match result {
Ok(Ok(())) => println!("Insert successful"),
Ok(Err(e)) => println!("Database error: {:?}", e),
Err(e) => println!("Call failed: {:?}", e),
}
Detailed Matching
match client.insert::<User>(User::table_name(), user, None).await {
Ok(Ok(())) => {
println!("Insert successful");
}
Ok(Err(db_error)) => {
match db_error {
IcDbmsError::Query(QueryError::PrimaryKeyConflict) => {
println!("User already exists");
}
IcDbmsError::Query(QueryError::BrokenForeignKeyReference) => {
println!("Referenced record doesn't exist");
}
IcDbmsError::Validation(msg) => {
println!("Validation error: {}", msg);
}
_ => {
println!("Database error: {:?}", db_error);
}
}
}
Err(call_error) => {
println!("Failed to call canister: {:?}", call_error);
}
}
Helper Function Pattern
fn handle_db_error(error: IcDbmsError) -> String {
match error {
IcDbmsError::Query(QueryError::PrimaryKeyConflict) =>
"Record with this ID already exists".to_string(),
IcDbmsError::Query(QueryError::BrokenForeignKeyReference) =>
"Referenced record not found".to_string(),
IcDbmsError::Query(QueryError::ForeignKeyConstraintViolation) =>
"Cannot delete: record has dependencies".to_string(),
IcDbmsError::Validation(msg) =>
format!("Invalid data: {}", msg),
_ =>
format!("Unexpected error: {:?}", error),
}
}
// Usage
let result = client.insert::<User>(User::table_name(), user, None).await;
match result {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(handle_db_error(e)),
Err(e) => Err(format!("Call failed: {:?}", e)),
}
Retry Pattern for Transient Errors
Network-level errors (outer Result) may be transient. Database errors (inner Result) are deterministic and should not be retried.
async fn insert_with_retry<T: Table>(
client: &impl Client,
table: &str,
record: T::InsertRequest,
max_retries: u32,
) -> Result<(), String> {
for attempt in 0..max_retries {
match client.insert::<T>(table, record.clone(), None).await {
Ok(Ok(())) => return Ok(()),
Ok(Err(e)) => {
// Database errors are deterministic - don't retry
return Err(format!("Database error: {:?}", e));
}
Err(call_err) => {
// Call errors might be transient - retry
if attempt < max_retries - 1 {
println!("Attempt {} failed, retrying...", attempt + 1);
continue;
}
return Err(format!("Call failed after {} attempts: {:?}", max_retries, call_err));
}
}
}
unreachable!()
}