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:
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));
}
}
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"));
}
}
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 |
|
|
Blockchain Service |
|
|
Block Watcher Service |
|
|
Filter Service |
|
|
Notification Service |
|
|
Repository |
|
|
Script Utils |
|
|
Trigger Service |
|
|
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 |
External API Boundaries |
Always convert to your domain’s error type to avoid leaking implementation details |
Error Creation Examples
let error = ConfigError::validation_error(
"Invalid network configuration",
None,
Some(HashMap::from([
("network", "ethereum"),
("field", "rpc_url")
]))
);
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 performance -
fields(…)
- Adds specific fields we want to track (like network slug) -
tracing::debug!
- Adds debug-level spans for important operations