# How Macro Handles Errors in New Code: The `rootcause` Pattern Explained

> Learn how Macro handles errors in new Rust code using the rootcause crate. Discover Result<T, Report> types, domain-specific error codes, and automatic tracing.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: how-to-guide
- Published: 2026-08-21

---

**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`](https://github.com/macro-inc/macro/blob/main/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`.

```rust
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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/service/entity_mutation.rs) (lines 100-112), the `access_failure` helper demonstrates this mapping:

```rust
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.

```rust
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`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/service/entity_mutation.rs) (line 158), this preserves the original error while providing diagnostic details:

```rust
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`](https://github.com/macro-inc/macro/blob/main/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.

```rust
#[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:

1. Domain functions return `Result<T, Report>`
2. Service layers map `Report` to `EntityMutationErrorCode` using pattern matching on underlying causes
3. Axum handlers convert error codes to HTTP responses

```rust
fn map_error(report: Report) -> MyErrorCode {
    if report.is::<JwtError>() { MyErrorCode::InvalidJwt }
    else { MyErrorCode::Internal }
}

```

## Summary

- **Mandatory `rootcause` usage**: New code in `macro-inc/macro` must use `rootcause::Report` instead of `anyhow`, as enforced by [`docs/STYLE_GUIDE.md`](https://github.com/macro-inc/macro/blob/main/docs/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 `EntityMutationErrorCode` in [`crates/entity_mutation/src/models.rs`](https://github.com/macro-inc/macro/blob/main/crates/entity_mutation/src/models.rs) to 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 `Report` to 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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/docs/STYLE_GUIDE.md) (lines 80-82).