Monitor

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_mainnet

Error 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

ModuleError TypeDescription
**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

ScenarioApproach
Crossing Domain BoundariesConvert to domain-specific error type using custom error constructors
Within Same DomainUse .with_context() to add information while maintaining error type
External API BoundariesAlways 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:

  1. skip_all - Skips automatic instrumentation of function parameters for performance
  2. fields(...) - Adds specific fields we want to track (like network slug)
  3. tracing::debug! - Adds debug-level spans for important operations