Return Structured Errors

return-structured-errors

Summary

String-only errors hide the stable facts callers need for branching, retrying, logging, and support. Return structured error values with machine-readable kinds and context, then render audience-specific messages at the boundary.

Problem

String errors and broad catch-all errors make callers parse prose or give up. The user-visible message may be good enough for a person, but code still needs a stable kind, recoverable fields, source error, and sometimes a correlation or instance identifier.

Preferred Move

Return errors with structure at API boundaries. Use enums, structs, problem-detail objects, error kinds, source chains, and typed fields so callers can branch, retry, show useful messages, or attach support context without parsing text.

Tradeoff

Applications can aggregate errors internally when no caller can act on the variants. Reusable libraries and public service boundaries should not expose only anyhow::Error, String, or unstructured JSON when callers need stable behavior.

Agent Instruction

When designing an error boundary, include stable error kind, actionable fields, source context, and
correlation or instance identity when needed. Keep display text helpful, but do not make prose the
only machine-readable contract.

Examples

Bad: callers can only inspect text.

pub fn load_pattern(id: PatternId) -> Result<Pattern, String> {
    repository::load(id).map_err(|err| format!("could not load pattern: {err}"))
}

Good: callers can match the kind and preserve the source.

pub enum LoadPatternError {
    NotFound { id: PatternId },
    Repository { id: PatternId, source: RepositoryError },
}

pub fn load_pattern(id: PatternId) -> Result<Pattern, LoadPatternError> {
    repository::load(id).map_err(|source| LoadPatternError::Repository { id, source })
}

References

Source Use Note
C-GOOD-ERR supports Error types should be meaningful and well-behaved.
RFC 9457 adapts HTTP APIs can expose machine-readable problem details.