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

Module Error Type Description

Configuration

ConfigError

  • ValidationError - Configuration validation failures

  • ParseError - Configuration parsing issues

  • FileError - File system related errors

  • Other - Unclassified 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

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:

  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