Join our community of builders on

Telegram!Telegram

Error Handling

Development Documentation

You're viewing documentation for unreleased features from the main branch. For production use, see the latest stable version (v1.1.x).

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