How Macro Handles Errors in New Code: The `rootcause` Pattern Explained
Macro enforces a strict error-handling strategy requiring all new Rust code to use the rootcause crate, returning Result<T, Report> types with domain-specific error codes and automatic tracing instrumentation.
The macro-inc/macro repository implements a disciplined approach to failure management that prioritizes observability and API clarity. Instead of generic error bubbling, the codebase mandates structured error variants, contextual attachment, and clean separation between domain failures and HTTP transport concerns.
The rootcause Crate: Mandatory for New Rust Code
According to the project's style guide defined in docs/STYLE_GUIDE.md, developers must use the rootcause crate rather than the more common anyhow library when writing new code. This requirement appears in the error handling section (CS-46) at lines 33-36, establishing a consistent pattern across all services.
The Result<T, Report> Return Type Convention
Every public function capable of failure must return a Result whose error type is rootcause::Report (or Report<E> for specific error enums). This convention ensures that callers receive rich error information including stack traces and structured context. The pattern appears throughout the codebase, particularly in domain services like document_storage_service.
use rootcause::{Report, report};
pub async fn create_document(
ctx: &Context,
payload: DocumentPayload,
) -> Result<DocumentId, Report> {
if payload.title.is_empty() {
rootcause::bail!("title cannot be empty");
}
let id = db::insert_document(&ctx.db, &payload)
.await
.map_err(|e| report!(e).attach("operation" => "insert_document"))?;
Ok(id)
}
Why rootcause Over anyhow
While anyhow provides convenient error handling for applications, Macro's style guide specifically mandates rootcause because it supports the domain-specific error code mapping required for API consistency. The Report type maintains the full error chain while allowing conversion to service-specific error codes before reaching HTTP handlers.
Structuring Errors for Domain Clarity
Macro's error handling strategy emphasizes explicit variants over opaque internal errors. This approach enables precise failure classification and appropriate client responses.
Wrapping Third-Party Errors in Dedicated Variants
External library errors are never collapsed into generic "internal error" messages. As specified in docs/STYLE_GUIDE.md (lines 74-76), each third-party failure receives its own enum variant such as JwtError or RdkafkaError. This pattern allows calling code to react to specific failure modes rather than handling all external errors uniformly.
When a JWT validation fails, for example, the error propagates as a distinct JwtError variant rather than a stringified internal failure, enabling the service layer to map it to an appropriate HTTP 401 or 403 response.
Domain-Specific Error Codes and API Mapping
Each service defines an ErrorCode or EntityMutationErrorCode enum that maps internal failures to API-friendly codes. In crates/entity_mutation/src/models.rs (lines 57-103), the EntityMutationErrorCode enum demonstrates this pattern with variants that hold Sentinel placeholders and implement From<rootcause::Report> for conversion.
The conversion functions translate rootcause::Report instances into the appropriate error code before reaching the Axum router layer. In services/document_storage_service/src/service/entity_mutation.rs (lines 100-112), the access_failure helper demonstrates this mapping:
fn access_failure(error: AccessError) -> EntityMutationErrorCode {
match error {
AccessError::Forbidden => EntityMutationErrorCode::forbidden(rootcause::report!(error)),
AccessError::NotFound => EntityMutationErrorCode::not_found(rootcause::report!(error)),
AccessError::InvalidInput => EntityMutationErrorCode::invalid(rootcause::report!(error)),
AccessError::Internal => EntityMutationErrorCode::internal(rootcause::report!(error)),
}
}
Instrumentation and Observability
Modern error handling requires more than propagation—it demands visibility. Macro integrates structured logging and tracing directly into the error lifecycle.
Automatic Stack Traces with rootcause::report!
The rootcause::report! macro creates a Report while automatically capturing stack traces and additional context. This macro is the preferred mechanism for converting ordinary errors into Report instances. When applied with the ? operator, it preserves the original error while enriching it with diagnostic information.
some_lib::call(...).map_err(|e| rootcause::report!(e))?;
Context Attachment with .attach()
When errors require operational context (such as which entity caused a failure), the report can be enriched using the .attach() method. As shown in services/document_storage_service/src/service/entity_mutation.rs (line 158), this preserves the original error while providing diagnostic details:
rootcause::report!(entity).attach(operation)
Tracing Integration with #[instrument(err)]
Every Result-returning function carries the #[instrument(err)] attribute, satisfying the observability rules in docs/STYLE_GUIDE.md (lines 80-82). When a function returns a Report, the error automatically logs as a structured field, enabling correlation across distributed traces without manual logging boilerplate.
#[instrument(skip(state), err)]
async fn handler(
State(state): State<AppState>,
Json(req): Json<CreateDocRequest>,
) -> Result<impl IntoResponse, Report> {
let doc_id = state
.document_service
.create_document(&state.ctx, req.into())
.await?;
Ok(Json(json!({ "id": doc_id })))
}
Error Propagation to the HTTP Layer
The final stage of error handling occurs at the service boundary. Axum routers convert internal Report or ErrorCode values into JSON payloads with appropriate HTTP status codes (400, 403, 404, 500). This conversion lives in outbound router modules, keeping domain logic free of transport concerns.
The pipeline flows as follows:
- Domain functions return
Result<T, Report> - Service layers map
ReporttoEntityMutationErrorCodeusing pattern matching on underlying causes - Axum handlers convert error codes to HTTP responses
fn map_error(report: Report) -> MyErrorCode {
if report.is::<JwtError>() { MyErrorCode::InvalidJwt }
else { MyErrorCode::Internal }
}
Summary
- Mandatory
rootcauseusage: New code inmacro-inc/macromust userootcause::Reportinstead ofanyhow, as enforced bydocs/STYLE_GUIDE.md. - Explicit error variants: Third-party errors receive dedicated enum variants (e.g.,
JwtError) rather than generic strings, enabling precise error classification. - Domain error codes: Services define specific error enums like
EntityMutationErrorCodeincrates/entity_mutation/src/models.rsto map internal failures to API-compatible responses. - Rich context: The
rootcause::report!macro captures stack traces, while.attach()adds operational context without losing the original error. - Automatic tracing: The
#[instrument(err)]attribute ensures all errors log as structured fields for observability. - Clean boundaries: Axum routers handle the conversion from
Reportto HTTP responses, maintaining separation between domain and transport layers.
Frequently Asked Questions
Why does Macro require rootcause instead of anyhow?
The rootcause crate provides the structured error code mapping and stack trace capture necessary for Macro's API requirements. While anyhow excels at application-level error handling, rootcause enables the domain-specific error classification mandated by the style guide in docs/STYLE_GUIDE.md, allowing precise mapping to HTTP status codes and client-facing error messages.
How are third-party library errors handled?
External library errors are wrapped in dedicated enum variants rather than converted to generic strings. As specified in the style guide (lines 74-76), each third-party failure type (such as JWT or Kafka errors) receives its own variant like JwtError or RdkafkaError. This pattern preserves type information, allowing callers to match on specific failure modes and translate them to appropriate domain error codes.
What is the role of EntityMutationErrorCode?
EntityMutationErrorCode serves as the bridge between internal Rust errors and external API responses. Defined in crates/entity_mutation/src/models.rs, this enum implements From<rootcause::Report> to convert internal failures into standardized error codes. The conversion happens in service layers such as services/document_storage_service/src/service/entity_mutation.rs, ensuring HTTP handlers receive already-categorized errors ready for JSON serialization.
How does tracing work with error handling?
Every function returning a Result uses the #[instrument(err)] attribute, which automatically captures returned Report values as structured log fields when errors occur. Combined with the rootcause::report! macro's automatic stack trace generation, this provides complete observability without manual logging code, satisfying the tracing requirements in docs/STYLE_GUIDE.md (lines 80-82).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →