# How Automattic/harper Handles Errors: A Layered Rust Error Handling Strategy

> Discover how Automattic/harper leverages Rust Result and anyhow for robust error handling. Learn its layered strategy for rich diagnostics across CLI, LSP, and web interfaces.

- Repository: [Automattic/harper](https://github.com/Automattic/harper)
- Tags: deep-dive
- Published: 2026-08-01

---

**Automattic/harper uses a multi-layered error handling approach combining Rust's native `Result` types with the `anyhow` crate for ergonomic error propagation, while preserving rich diagnostic information across CLI, LSP, and web interfaces.**

The Harper grammar and spelling engine implements a sophisticated error management strategy that balances type safety with developer ergonomics. By separating concerns across core libraries, application crates, transport layers, and user interfaces, the codebase maintains clean error propagation without sacrificing actionable feedback for end users.

## Core Error Handling Philosophy

Harper's architecture divides error responsibilities into distinct layers. This separation allows internal parsing failures to remain detailed and typed, while application-level code benefits from flexible, context-rich error handling.

The foundation rests on Rust's `Result<T, E>` type, with higher-level crates wrapping these results in `anyhow::Result<T>` to enable seamless error chaining and contextual reporting.

## Layer 1: Core Library Error Types (`harper-core`, `harper-pos-utils`)

Internal Harper components use typed error enums to represent specific failure modes. These errors implement `std::error::Error` and propagate via the `?` operator.

Common error categories in the core include:

- **ParseError** – Tokenization or grammatical parsing failures
- **TaggerError** – Part-of-speech tagging failures
- **Dictionary lookup errors** – Missing or corrupted dictionary entries

These internal errors remain strongly typed to preserve semantic meaning for debugging, while still converting cleanly into generic errors when crossing crate boundaries.

## Layer 2: Application-Level Error Handling (`harper-cli`, `harper-ls`)

Application crates leverage the `anyhow` crate for ergonomic error management. Key patterns include:

- **`anyhow::bail!`** – Immediate error generation for non-recoverable conditions
- **`anyhow::anyhow!`** – Structured error creation with formatting
- **`.context()`** – Attaching descriptive text to error chains
- **`?` propagation** – Implicit error conversion and bubbling

### CLI Error Entry Point

In [`harper-cli/src/main.rs`](https://github.com/Automattic/harper/blob/main/harper-cli/src/main.rs), the entry point declares an `anyhow::Result` return type:

```rust
fn main() -> anyhow::Result<()> {
    // Parse CLI args; any parsing error bubbles up with context.
    let opts = Opt::parse().context("Failed to parse command-line options")?;

    // Load the dictionary; a missing file becomes a clear error message.
    let dict = load_dict(&opts.dict_path)
        .context("Unable to load the user dictionary")?;

    // Run the linter; any lint failure is turned into a non-zero exit code.
    let lint_result = run_linter(&dict, &opts.input)
        .context("Linting failed")?;

    // Propagate any unexpected errors to the OS.
    Ok(lint_result)
}

```

Each operation attaches contextual information, producing error messages like "Unable to load the user dictionary: No such file or directory".

### Language Server Error Handling

The `harper-ls` crate similarly uses `anyhow` for asynchronous operations. In [`harper-ls/src/backend.rs`](https://github.com/Automattic/harper/blob/main/harper-ls/src/backend.rs), dictionary loading includes path-specific context:

```rust
async fn load_file_dictionary(&self, uri: &Uri) -> anyhow::Result<MutableDictionary> {
    let path = self.fileify_path(uri)
        .ok_or_else(|| anyhow!("Unable to convert URI to file path."))?;
    let dict = MutableDictionary::load(&path)
        .await
        .with_context(|| format!("Failed to load dictionary from {}", path.display()))?;
    Ok(dict)
}

```

The `ok_or_else` + `with_context` combination transforms low-level IO errors into actionable messages that include the specific file path involved.

## Layer 3: Diagnostic Conversion and LSP Integration

Errors transition from internal representations to user-visible diagnostics through [`harper-ls/src/diagnostics.rs`](https://github.com/Automattic/harper/blob/main/harper-ls/src/diagnostics.rs). This module converts Harper's internal `Lint` structures into Language Server Protocol (LSP) diagnostic objects:

```rust
pub fn lint_to_diagnostic(lint: &Lint) -> lsp::Diagnostic {
    lsp::Diagnostic {
        range: span_to_range(&lint.span),
        severity: Some(lsp::DiagnosticSeverity::Warning),
        code: Some(lsp::NumberOrString::String(lint.rule_name.clone())),
        source: Some("harper".into()),
        message: lint.message.clone(),
        ..Default::default()
    }
}

```

This conversion preserves:
- **Source location** – Character ranges mapped from Harper spans
- **Severity levels** – Warnings for style issues, errors for grammar
- **Rule identification** – The specific lint rule that triggered
- **Human-readable messages** – Clear explanations of the issue

## Layer 4: User-Facing Error Presentation

Different interfaces receive appropriately formatted error information:

| Interface | Error Format | Implementation |
|-----------|-----------|----------------|
| **CLI** | Colored terminal output via `eprintln!`, with `--no-color` stripping | [`harper-cli/src/main.rs`](https://github.com/Automattic/harper/blob/main/harper-cli/src/main.rs) |
| **LSP clients** (VS Code, Neovim) | Inline diagnostics with hover details | [`harper-ls/src/diagnostics.rs`](https://github.com/Automattic/harper/blob/main/harper-ls/src/diagnostics.rs) |
| **Web UI** | Error banners from serialized diagnostic payloads | Tauri IPC layer |

The CLI specifically strips ANSI color codes when stdout isn't a TTY or when the user requests plain output, ensuring compatibility with piping and log capture.

## Key Error Handling Patterns in Harper

**Propagation with `?`** – Most functions return `Result<…, impl std::error::Error>` and forward failures upward without explicit match statements.

**Contextualization** – The `.context("…")` and `.with_context(|| …)` methods from `anyhow` embed operation-specific details into error traces.

**Immediate bail** – For unsupported operations or invalid configuration, `anyhow::bail!("Unknown dialect: {}", dialect)` terminates execution with a clear message.

**Logging vs. Reporting** – Internal diagnostics use `eprintln!` for development visibility, while user-facing errors flow through `anyhow` channels to appropriate interfaces.

## Summary

- Harper implements **layered error handling** with typed core errors and flexible `anyhow` wrappers at application boundaries
- **Core libraries** (`harper-core`) use specific error enums for parsing and tagging failures
- **Application crates** (`harper-cli`, `harper-ls`) rely on `anyhow::Result<T>` with `.context()` for rich error messages
- **Diagnostic conversion** in [`harper-ls/src/diagnostics.rs`](https://github.com/Automattic/harper/blob/main/harper-ls/src/diagnostics.rs) translates internal lints to LSP-compatible diagnostics
- **User interfaces** receive appropriately formatted errors: terminal colors for CLI, structured diagnostics for editors, JSON payloads for web

## Frequently Asked Questions

### What error handling crate does Automattic/harper use?

Harper uses the **anyhow** crate for application-level error handling in `harper-cli` and `harper-ls`. This provides ergonomic error propagation through `?` operator support, `bail!` for early returns, and `Context` trait methods for attaching descriptive information to errors.

### How does harper-ls report errors to editors?

The language server converts internal errors into LSP diagnostics through [`harper-ls/src/diagnostics.rs`](https://github.com/Automattic/harper/blob/main/harper-ls/src/diagnostics.rs). The `lint_to_diagnostic` function transforms Harper's `Lint` structures into `lsp::Diagnostic` objects containing severity, message, source location, and rule identification that editors display inline.

### Where does Harper handle CLI argument parsing errors?

CLI argument parsing occurs in [`harper-cli/src/main.rs`](https://github.com/Automattic/harper/blob/main/harper-cli/src/main.rs) using `clap` with `anyhow` context wrapping. The `.context("Failed to parse command-line options")` call ensures parsing failures produce clear, actionable error messages rather than raw clap output.

### Does Harper use panic or Result for error handling?

Harper exclusively uses `Result`-based error handling. The codebase avoids `panic!` for expected failure modes, reserving panics only for unrecoverable bugs. All IO operations, parsing, and dictionary loading return `Result` types that propagate through the `?` operator or `anyhow` conversion.