Error Handling
Overview
The OpenZeppelin Monitor uses a structured error handling system that provides rich context and tracing capabilities across service boundaries. Let’s start with a real-world example of how errors flow through our system.
Error Flow Example
Let’s follow how an error propagates through our blockchain monitoring system:
Low-level Transport (endpoint_manager.rs)
// Creates basic errors with specific context
async fn send_raw_request(...) -> Result<Value, anyhow::Error> {
    let response = client.post(...)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to send request: {}", e))?;
    if !status.is_success() {
        return Err(anyhow::anyhow!("HTTP error {}: {}", status, error_body));
    }
}Client Layer (evm/client.rs)
// Adds business context to low-level errors
async fn get_transaction_receipt(...) -> Result<EVMTransactionReceipt, anyhow::Error> {
    let response = self.alloy_client
        .send_raw_request(...)
        .await
        .with_context(|| format!("Failed to get transaction receipt: {}", tx_hash))?;
    if receipt_data.is_null() {
        return Err(anyhow::anyhow!("Transaction receipt not found"));
    }
}Filter Layer (evm/filter.rs)
// Converts to domain-specific errors
async fn filter_block(...) -> Result<Vec<MonitorMatch>, FilterError> {
    let receipts = match futures::future::join_all(receipt_futures).await {
        Ok(receipts) => receipts,
        Err(e) => {
            return Err(FilterError::network_error(
                format!("Failed to get transaction receipts for block {}", block_num),
                Some(e.into()),
                None,
            ));
        }
    };
}When this error occurs, it produces the following log:
ERROR filter_block: openzeppelin_monitor::utils::error: Error occurred,
    error.message: Failed to get transaction receipts for block 15092829,
    error.trace_id: a464d73c-5992-4cb5-a002-c8d705bfef8d,
    error.timestamp: 2025-03-14T09:42:03.412341+00:00,
    error.chain: Failed to get receipt for transaction 0x7722194b65953085fe1e9ec01003f1d7bdd6258a0ea5c91a59da80419513d95d
  Caused by: HTTP error 429 Too Many Requests: {"code":-32007,"message":"[Exceeded request limit per second]"}
 network: ethereum_mainnetError Structure
Error Context
Every error in our system includes detailed context information:
pub struct ErrorContext {
    /// The error message
    pub message: String,
    /// The source error (if any)
    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
    /// Unique trace ID for error tracking
    pub trace_id: String,
    /// Timestamp when the error occurred
    pub timestamp: DateTime<Utc>,
    /// Optional key-value metadata
    pub metadata: HashMap<String, String>,
}Domain-Specific Error Types
| Module | Error Type | Description | 
|---|---|---|
**Configuration** | ConfigError | * ValidationError - Configuration validation failures * ParseError - Configuration parsing issues * FileError - File system related errors * Other - Unclassified errors | 
**Security** | SecurityError | * ValidationError - Security validation failures * ParseError - Security data parsing issues * NetworkError - Security service connectivity issues * Other - Unclassified security-related errors | 
**Blockchain Service** | BlockChainError | * ConnectionError - Network connectivity issues * RequestError - Malformed requests or invalid responses * BlockNotFound - Requested block not found * TransactionError - Transaction processing failures * InternalError - Internal client errors * ClientPoolError - Client pool related issues * Other - Unclassified errors | 
**Block Watcher Service** | BlockWatcherError | * SchedulerError - Block watching scheduling issues * NetworkError - Network connectivity problems * ProcessingError - Block processing failures * StorageError - Storage operation failures * BlockTrackerError - Block tracking issues * Other - Unclassified errors | 
**Filter Service** | FilterError | * BlockTypeMismatch - Block type validation failures * NetworkError - Network connectivity issues * InternalError - Internal processing errors * Other - Unclassified errors | 
**Notification Service** | NotificationError | * NetworkError - Network connectivity issues * ConfigError - Configuration problems * InternalError - Internal processing errors * ExecutionError - Script execution failures * Other - Unclassified errors | 
**Repository** | RepositoryError | * ValidationError - Data validation failures * LoadError - Data loading issues * InternalError - Internal processing errors * Other - Unclassified errors | 
**Script Utils** | ScriptError | * NotFound - Resource not found errors * ExecutionError - Script execution failures * ParseError - Script parsing issues * SystemError - System-level errors * Other - Unclassified errors | 
**Trigger Service** | TriggerError | * NotFound - Resource not found errors * ExecutionError - Trigger execution failures * ConfigurationError - Trigger configuration issues * Other - Unclassified errors | 
**Monitor Executor** | MonitorExecutionError | * NotFound - Resource not found errors * ExecutionError - Monitor execution failures * Other - Unclassified errors | 
Error Handling Guidelines
When to Use Each Pattern
| Scenario | Approach | 
|---|---|
| Crossing Domain Boundaries | Convert to domain-specific error type using custom error constructors | 
| Within Same Domain | Use .with_context() to add information while maintaining error type | 
| External API Boundaries | Always convert to your domain’s error type to avoid leaking implementation details | 
Error Creation Examples
Creating a Configuration Error without a source
let error = ConfigError::validation_error(
    "Invalid network configuration",
    None,
    Some(HashMap::from([
        ("network", "ethereum"),
        ("field", "rpc_url")
    ]))
);Creating a Configuration Error with a source
let io_error = std::io::Error::new(std::io::ErrorKind::Other, "Failed to read file");
let error = ConfigError::validation_error(
    "Invalid network configuration",
    Some(io_error.into()),
    None
);Tracing with #[instrument]
#[instrument(skip_all, fields(network = %_network.slug))]
async fn filter_block(
    &self,
    client: &T,
    _network: &Network,
    block: &BlockType,
    monitors: &[Monitor],
) -> Result<Vec<MonitorMatch>, FilterError> {
    tracing::debug!("Processing block {}", block_number);
    // ...
}Key aspects:
skip_all- Skips automatic instrumentation of function parameters for performancefields(...)- Adds specific fields we want to track (like network slug)tracing::debug!- Adds debug-level spans for important operations