Lore error handling standards¶
This document defines the standard patterns for error handling and error propagation across the Lore codebase.
Overview¶
Lore uses a layered error handling approach:
- Domain-specific error enums — Defined per module using
thiserror. - Extension traits — For error transformation with automatic logging.
- Public error interface — The
LoreErrorenum exposed to API consumers (legacy; see section 4). - EventError trait — Translates internal errors to public errors (legacy; see section 4).
The canonical contract a C API consumer reads on a failure is the FFI error code on status, the return value, and the error detail on the Complete event. See section 4. LoreError and EventError are legacy and kept only for transition.
1. Defining error types¶
All error types MUST use thiserror. Each module defines its own error enum:
#[derive(Debug, Error, PartialEq)]
pub enum ModuleError {
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Task failed")]
TaskFailure,
}
Guidelines:
- Use
#[derive(Debug, Error)]at minimum; addPartialEqfor testability. - Error messages should be user-readable (they appear in logs).
- Use tuple variants for dynamic context:
NotFound(String),RemoteConnect(Context).
2. Public error interface (LoreError)¶
Defined in lore-revision/src/interface.rs. LoreError is the public error code returned across the FFI boundary; every internal error translates to one of its variants:
| Variant | Value | Meaning |
|---|---|---|
InvalidArguments |
1 | The arguments supplied to the operation were invalid. |
AddressNotFound |
2 | A content-addressable object wasn't found in any store. |
FileNotFound |
3 | A file path couldn't be resolved to a tracked node or found on disk. |
PayloadNotFound |
4 | A payload blob wasn't found for the associated hash. |
SlowDown |
5 | The backing store is overloaded; the caller should retry later. |
Oversized |
26 | A blob exceeded a size limit enforced by the caller or the protocol. |
Internal |
-1 | All other errors. |
The NotFound (101), AlreadyExists (102), and Connection (103) variants are legacy categories kept for transition and will be removed.
3. EventError trait¶
Defined in lore-revision/src/event.rs. Domain errors in lore-revision that surface to users MUST implement this trait:
impl EventError for ModuleError {
fn translated(&self) -> LoreError {
match self {
ModuleError::NotFound(_) => LoreError::NotFound,
_ => LoreError::Internal,
}
}
fn inner(&self) -> String {
self.to_string()
}
}
4. FFI error reporting contract¶
This section describes what a consumer of the C API reads on a failure. It is the canonical contract; the older LoreError and EventError paths above are legacy and kept only for transition.
The code carries on status and the return value¶
A failed operation reports its FFI error code in two places:
- The synchronous entry points return the FFI error code as their
int32result, and0on success. - The
Completeevent carries the same code in itsstatusfield:0on success, the FFI error code on failure.
The synchronous return value and Complete.status always agree, because both derive from the same outcome. An asynchronous (_async) entry point returns void, so for those callers Complete.status is the only place the code arrives.
The error detail on the Complete event¶
The Complete event also carries a LoreErrorDetail in its error field. It is the empty default on success and the populated detail on failure:
| Field | Type | Meaning |
|---|---|---|
error_code |
i32 |
The error's FFI code. 0 on success, -1 for an internal error. |
message |
LoreString |
The error message. Empty on success. |
trace_locations |
LoreArray<LoreTraceLocation> |
The captured trace, one entry per location. Empty when no trace was captured. |
Each LoreTraceLocation holds a file, a line, a column, and a per-location context string. A consumer reconstructs where the error was created or forwarded from these entries, without server logs.
status and error.error_code always hold the same value, by construction.
error_code is canonical; error_type and LoreError are legacy¶
error_code(onLoreErrorDetail, and the equalComplete.status) is the canonical code a consumer reads. It is the error's FFI code.error_typeon the legacyLoreErrorEventData, and theLoreErrorenum, are legacy. They disagree witherror_codefor most errors. Do not use them for new consumers.
No mid-stream Error event on a terminal failure¶
The library no longer emits a mid-stream LORE_EVENT_ERROR event on a terminal failure. The full error detail arrives on the Complete event instead. A failing operation delivers exactly one error-bearing event: the enriched Complete.
Memory lifetime¶
The library owns all error-detail memory. The pointers a consumer reads from LoreErrorDetail and LoreTraceLocation (the strings and the trace array) are valid only for the single callback invocation that delivers the event. A consumer that keeps any of this data must copy it out before the callback returns.
5. Error extension traits¶
Defined in lore-revision/src/error.rs. These transform errors with automatic logging.
Usage¶
use lore_revision::error::{LoreResultExt, LoreErrorExt};
// Transform error type with ERROR-level logging
store.get(key).emit_map_err(BranchError::StoreFailure)?;
// Transform with DEBUG-level logging (for expected failures)
store.get(key).debug_map_err(BranchError::NotExist)?;
// Return error directly with logging
return BranchError::InvalidName.emit();
return BranchError::NotExist.debug();
When to use each¶
| Method | Log level | Use case |
|---|---|---|
emit_map_err |
ERROR | Unexpected failures |
debug_map_err |
DEBUG | Expected failures (not found, already exists) |
emit() |
ERROR | Direct error return |
debug() |
DEBUG | Direct error return for expected cases |
The traits check the execution context's failure flag to prevent duplicate log cascades.
6. Panics and unwrap¶
Never use unwrap(), expect(), or code that can panic in production code. This is especially critical in lore-server where a panic can crash the entire server process.
// DON'T DO THIS
let value = map.get(key).unwrap();
let parsed: i32 = input.parse().expect("should be valid");
// DO THIS - propagate errors with logging
let value = map.get(key).ok_or(MyError::NotFound).debug()?;
let parsed: i32 = input.parse().emit_map_err(MyError::InvalidInput)?;
Acceptable uses of unwrap():
- Tests (where panics are expected failure modes).
- Static initialization where failure is unrecoverable.
// Acceptable: regex is compile-time validated
static RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\d+$").unwrap() // Infallible: regex is valid
});
7. Crate-specific patterns¶
Full pattern (thiserror + EventError + extension traits)¶
- lore-revision, lore
thiserror only (no EventError)¶
- lore-server — Uses
tracing; errors become gRPC/HTTP status codes. - lore-aws — AWS-specific errors with a generic type parameter.
- lore-base, lore-client, lore-credential, lore-error-set, lore-storage, lore-telemetry, lore-transport.
No custom error types¶
- lore-proto, lore-macro, lore-notification, lore-hashicorp, lore-chaos-client, lore-capi.
anyhow usage¶
anyhow is allowed in binaries only, not libraries:
| Crate type | Error handling |
|---|---|
Libraries (lore-revision, lore-aws, and others) |
thiserror with typed errors |
Binaries (lore-server, CLI tools) |
anyhow allowed for convenience |
Libraries must expose typed errors so callers can match on specific error variants. Binaries are the end of the error chain and can use anyhow for simpler error aggregation.
8. Lore server errors¶
In lore-server gRPC handlers, use warn_map_err, warn_error_to_status, or warn_mapped_error_status when converting internal errors into a gRPC Status. All three log the original error at WARN level with additional structured fields, ensuring the cause and response are visible in our observability for investigation.
Prefer these helpers when the resulting gRPC status code is considered a server error as per the function is_code_considered_server_error (for example, an Internal status). These represent unexpected failures where observability over the original error matters.
Don't use them for expected, user-caused errors (for example, NotFound, InvalidArgument, AlreadyExists) where the status code alone is sufficient and WARN-level logging would be noise.
warn_map_err— Use when you can chain directly with?on aResult.warn_error_to_status— Use when you already have the error value and need theStatusbefore returning.warn_mapped_error_status— Use when you have already mapped the error to aStatus(for example, inside amap_errclosure where the mapping and logging steps must be done independently).
9. Best practices¶
- Never panic in production code — Avoid
unwrap(),expect(), and panic-inducing code. - Always use thiserror for error enum definitions in libraries.
- Use anyhow only in binaries — Libraries must have typed errors.
- Implement EventError for errors reaching the public API (
lore-revisionandlore). - Use emit_map_err for unexpected failures.
- Use debug_map_err for expected/recoverable conditions.
- Use tracing in server code, Lore macros in library code.