Sentinel uses Rust’s Result type for error handling, with a custom SentinelError enum that categorizes all possible failure modes. This guide explains error types, how to handle them, and best practices for robust error management.
The SentinelError Type
All Sentinel operations return Result<T, SentinelError>. The error enum covers all failure scenarios:
pub enum SentinelError {
// I/O operations failed (file system, network, etc.)
Io { source: std::io::Error },
// JSON serialization/deserialization failed
Json { source: serde_json::Error },
// Document not found in collection
DocumentNotFound { id: String, collection: String },
// Collection not found in store
CollectionNotFound { name: String },
// Document already exists (for operations that require uniqueness)
DocumentAlreadyExists { id: String, collection: String },
// Invalid document ID format
InvalidDocumentId { id: String },
// Invalid collection name format
InvalidCollectionName { name: String },
// Store is corrupted or in an invalid state
StoreCorruption { reason: String },
// Transaction failed
TransactionFailed { reason: String },
// Lock acquisition failed
LockFailed { reason: String },
// Encryption/decryption operation failed
CryptoFailed { operation: String },
// Configuration error
ConfigError { message: String },
// Document hash verification failed
HashVerificationFailed { id: String, reason: String },
// Document signature verification failed
SignatureVerificationFailed { id: String, reason: String },
// Generic error for unexpected conditions
Internal { message: String },
}Error Categories
Sentinel errors fall into several categories based on their cause and how you should handle them.
I/O Errors
File system operations can fail for various reasons:
use sentinel_dbms::{Store, SentinelError};
#[tokio::main]
async fn handle_io_errors() {
match Store::new("/root/protected", None).await {
Ok(_) => println!("Store created"),
Err(SentinelError::Io { source }) => {
match source.kind() {
std::io::ErrorKind::PermissionDenied => {
eprintln!("Permission denied: {}", source);
}
std::io::ErrorKind::NotFound => {
eprintln!("Path not found: {}", source);
}
_ => {
eprintln!("I/O error: {}", source);
}
}
}
Err(e) => eprintln!("Other error: {}", e),
}
}Common I/O error causes include permission denied when creating directories, disk full when writing documents, and network errors on remote filesystems.
JSON Errors
Serialization and deserialization can fail with malformed data:
use sentinel_dbms::{Store, SentinelError};
#[tokio::main]
async fn handle_json_errors() {
let store = Store::new("./data", None).await.unwrap();
let collection = store.collection("test").await.unwrap();
// If someone manually edited a JSON file incorrectly...
match collection.get("corrupted-doc").await {
Ok(Some(doc)) => println!("Found: {:?}", doc),
Ok(None) => println!("Not found"),
Err(SentinelError::Json { source }) => {
eprintln!("Invalid JSON: {}", source);
// The file exists but contains invalid JSON
}
Err(e) => eprintln!("Other error: {}", e),
}
}Validation Errors
Invalid document IDs or collection names produce validation errors:
use sentinel_dbms::{Store, SentinelError};
use serde_json::json;
#[tokio::main]
async fn handle_validation_errors() {
let store = Store::new("./data", None).await.unwrap();
let collection = store.collection("users").await.unwrap();
match collection.insert("invalid/id", json!({})).await {
Ok(()) => println!("Created"),
Err(SentinelError::InvalidDocumentId { id }) => {
eprintln!("Invalid document ID '{}': contains path separator", id);
}
Err(e) => eprintln!("Other error: {}", e),
}
match store.collection("CON").await {
Ok(_) => println!("Collection created"),
Err(SentinelError::InvalidCollectionName { name }) => {
eprintln!("Invalid collection name '{}': Windows reserved name", name);
}
Err(e) => eprintln!("Other error: {}", e),
}
}Cryptographic Errors
Crypto operations can fail for several reasons:
use sentinel_dbms::{Store, SentinelError};
#[tokio::main]
async fn handle_crypto_errors() {
// Wrong passphrase for existing store
match Store::new("./encrypted-data", Some("wrong-password")).await {
Ok(_) => println!("Store opened"),
Err(SentinelError::CryptoFailed { operation }) => {
eprintln!("Crypto error: {}", operation);
// Likely decryption failed due to wrong passphrase
}
Err(e) => eprintln!("Other error: {}", e),
}
}Hash Verification Errors
Hash verification occurs when the computed hash doesn’t match the stored hash:
use sentinel_dbms::{Store, SentinelError, VerificationOptions};
#[tokio::main]
async fn handle_hash_errors() {
let store = Store::new("./data", None).await.unwrap();
let collection = store.collection("users").await.unwrap();
let options = VerificationOptions::strict();
match collection.get_with_verification("alice", &options).await {
Ok(Some(doc)) => println!("Found: {:?}", doc),
Ok(None) => println!("Not found"),
Err(SentinelError::HashVerificationFailed { id, reason }) => {
eprintln!("Hash verification failed for document '{}': {}", id, reason);
// Document data may be corrupted or modified
}
Err(e) => eprintln!("Other error: {}", e),
}
}This error can occur when:
- Document file is corrupted or modified externally
- Storage media has errors
- Document was tampered with
- Bug in hash computation (shouldn’t occur in practice)
Signature Verification Errors
Signature verification fails when the digital signature can’t be verified with the public key:
use sentinel_dbms::{Store, SentinelError, VerificationOptions};
#[tokio::main]
async fn handle_signature_errors() {
let store = Store::new("./data", Some("passphrase")).await.unwrap();
let collection = store.collection("users").await.unwrap();
let options = VerificationOptions::strict();
match collection.get_with_verification("alice", &options).await {
Ok(Some(doc)) => println!("Found: {:?}", doc),
Ok(None) => println!("Not found"),
Err(SentinelError::SignatureVerificationFailed { id, reason }) => {
eprintln!("Signature verification failed for document '{}': {}", id, reason);
// Document may be tampered with or signing key changed
}
Err(e) => eprintln!("Other error: {}", e),
}
}This error can occur when:
- Document data was modified after signing
- Different signing key was used
- Signing key file is corrupted or missing
- Store passphrase was changed between signing and verification
Store Corruption
Detected when internal data structures are invalid:
use sentinel_dbms::{Store, SentinelError};
#[tokio::main]
async fn handle_corruption() {
match Store::new("./data", Some("passphrase")).await {
Ok(_) => println!("Store opened"),
Err(SentinelError::StoreCorruption { reason }) => {
eprintln!("Store corrupted: {}", reason);
// The .keys/signing_key.json may be damaged
}
Err(e) => eprintln!("Other error: {}", e),
}
}This error typically indicates:
- Missing required fields in encrypted key file
- Invalid hex encoding for encrypted data
- Inconsistent key file structure
- File system corruption affecting critical data
Using the Result Type Alias
Sentinel provides a Result type alias for convenience:
use sentinel_dbms::{Result, Store, Collection, Document};
async fn my_function() -> Result<Document> {
let store = Store::new("./data", None).await?;
let users = store.collection("users").await?;
users.get("alice").await?.ok_or_else(|| {
sentinel_dbms::SentinelError::DocumentNotFound {
id: "alice".to_string(),
collection: "users".to_string(),
}
})
}Verification Error Handling
Verification errors (hash and signature verification failures) are controlled by verification modes:
use sentinel_dbms::{Store, VerificationOptions, VerificationMode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new("./data", None).await?;
let collection = store.collection("users").await?;
// Strict mode: fail on verification errors (production default)
let strict_options = VerificationOptions::strict();
// Warning mode: log warnings but continue (auditing scenarios)
let warn_options = VerificationOptions::warn();
// Silent mode: skip verification entirely (performance-critical scenarios)
let silent_options = VerificationOptions::disabled();
// Custom configuration
let custom_options = VerificationOptions {
verify_signature: true,
verify_hash: false, // Only verify signature
signature_verification_mode: VerificationMode::Strict,
empty_signature_mode: VerificationMode::Warn,
hash_verification_mode: VerificationMode::Silent,
};
Ok(())
}See the Cryptography guide for more details on verification options and modes.
Pattern: Propagating Errors
Use the ? operator to propagate errors up the call stack:
use sentinel_dbms::{Store, Result};
use serde_json::json;
async fn create_user(store: &Store, id: &str, name: &str) -> Result<()> {
let users = store.collection("users").await?;
users.insert(id, json!({ "name": name })).await?;
Ok(())
}
async fn main_operation() -> Result<()> {
let store = Store::new("./data", None).await?;
create_user(&store, "alice", "Alice").await?;
create_user(&store, "bob", "Bob").await?;
Ok(())
}Pattern: Converting to Application Errors
Convert Sentinel errors to your application’s error type:
use sentinel_dbms::{Store, SentinelError};
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("Database error: {0}")]
Database(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
}
impl From<SentinelError> for AppError {
fn from(err: SentinelError) -> Self {
match err {
SentinelError::DocumentNotFound { id, .. } => {
AppError::NotFound(format!("Document '{}' not found", id))
}
SentinelError::InvalidDocumentId { id } => {
AppError::InvalidInput(format!("Invalid ID: {}", id))
}
SentinelError::InvalidCollectionName { name } => {
AppError::InvalidInput(format!("Invalid collection: {}", name))
}
SentinelError::HashVerificationFailed { id, reason } => {
AppError::Database(format!("Hash verification failed for document '{}': {}", id, reason))
}
SentinelError::SignatureVerificationFailed { id, reason } => {
AppError::Database(format!("Signature verification failed for document '{}': {}", id, reason))
}
SentinelError::StoreCorruption { reason } => {
AppError::Database(format!("Store corrupted: {}", reason))
}
other => AppError::Database(other.to_string()),
}
}
}
async fn get_user(store: &Store, id: &str) -> Result<String, AppError> {
let users = store.collection("users").await?;
let doc = users.get(id).await?.ok_or_else(|| {
AppError::NotFound(format!("User '{}' not found", id))
})?;
Ok(doc.data()["name"].as_str().unwrap_or("").to_string())
}Pattern: Handling Optional Documents
The get method returns Option<Document>, not an error for missing documents:
use sentinel_dbms::Store;
use serde_json::json;
async fn safe_get_or_create(
store: &Store,
collection_name: &str,
id: &str,
) -> sentinel_dbms::Result<sentinel_dbms::Document> {
let collection = store.collection(collection_name).await?;
match collection.get(id).await? {
Some(doc) => Ok(doc),
None => {
// Create default document
collection.insert(id, json!({
"created_automatically": true
})).await?;
// Fetch and return it
collection.get(id).await?.ok_or_else(|| {
sentinel_dbms::SentinelError::Internal {
message: "Document not found after creation".to_string(),
}
})
}
}
}Error Messages
All Sentinel errors implement Display and provide human-readable messages:
use sentinel_dbms::SentinelError;
fn format_error(error: SentinelError) -> String {
// All variants have descriptive messages
error.to_string()
}
// Example outputs:
// "I/O error: Permission denied"
// "Invalid document ID: user/admin"
// "Store corruption detected: stored signing key has an invalid length"
// "Cryptographic operation failed: decryption error"
// "Document 'alice' hash verification failed: Expected hash: abc..., Computed hash: xyz..."
// "Document 'alice' signature verification failed: Signature verification using public key failed"Best Practices for Error Handling
When working with errors in Sentinel, consider these practices:
Use Appropriate Verification Modes
Choose verification mode based on your use case:
- Production: Use
VerificationMode::Strictfor security - Auditing: Use
VerificationMode::Warnto detect issues without blocking - Performance: Use
VerificationMode::Silentwhen documents are known to be trustworthy - Mixed Collections: Use
VerificationMode::Warnfor empty signatures when some documents are signed
Provide Meaningful Error Context
When wrapping Sentinel errors, provide useful context:
async fn get_user_for_transaction(
store: &Store,
user_id: &str,
transaction_id: &str,
) -> Result<User, AppError> {
let collection = store.collection("users").await?;
let doc = collection.get(user_id).await?.ok_or_else(|| {
AppError::NotFound(format!(
"User '{}' not found for transaction '{}'",
user_id,
transaction_id
))
})?;
// Parse user data
Ok(serde_json::from_value(doc.data().clone()).map_err(|e| {
AppError::InvalidInput(format!(
"Invalid user data for transaction '{}': {}",
transaction_id,
e
))
})?)
}Handle Verification Errors Gracefully
For verification errors, consider whether to fail fast or continue processing:
use sentinel_dbms::{Store, VerificationOptions, VerificationMode};
async fn audit_collection(store: &Store) -> Result<(), AppError> {
let collection = store.collection("users").await?;
let options = VerificationOptions {
verify_signature: true,
verify_hash: true,
signature_verification_mode: VerificationMode::Warn,
empty_signature_mode: VerificationMode::Silent, // Allow unsigned documents
hash_verification_mode: VerificationMode::Warn,
};
// Stream all documents with warning mode
let mut stream = collection.all_with_verification(&options);
while let Some(doc_result) = stream.next().await {
match doc_result {
Ok(doc) => {
// Process document
println!("Audited: {}", doc.id());
}
Err(sentinel_dbms::SentinelError::HashVerificationFailed { id, reason })
| sentinel_dbms::SentinelError::SignatureVerificationFailed { id, reason }) => {
// Log but continue auditing
eprintln!("Verification failed for '{}': {}", id, reason);
// Don't fail the entire audit
}
Err(e) => {
// Other errors should fail the audit
return Err(AppError::Database(e.to_string()));
}
}
}
Ok(())
}Log Errors Appropriately
Use appropriate log levels for different error types:
use tracing::{error, warn, info};
async fn safe_operation(store: &Store) {
match store.collection("users").await {
Ok(collection) => {
info!("Collection accessed successfully");
}
Err(SentinelError::Io { source }) if source.kind() == std::io::ErrorKind::PermissionDenied => {
error!("Permission denied accessing collection");
// Critical: user intervention needed
}
Err(SentinelError::HashVerificationFailed { id, reason }) => {
warn!("Hash verification failed for document '{}': {}", id, reason);
// Warning: data integrity issue detected
}
Err(e) => {
error!("Unexpected error: {}", e);
// Critical: unknown error occurred
}
}
}Next Steps
Now that you understand error handling, explore:
- Store: Store creation and configuration
- Collection: Collection operations and error scenarios
- Document: Working with documents and verification errors
- Cryptography: Configuring verification modes