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

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, the entry point declares an anyhow::Result return type:

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, dictionary loading includes path-specific context:

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. This module converts Harper's internal Lint structures into Language Server Protocol (LSP) diagnostic objects:

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
LSP clients (VS Code, Neovim) Inline diagnostics with hover details 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 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. 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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →