feat: migrate error handling to terrors for precise error types #32

Closed
opened 2026-03-15 17:31:00 +00:00 by CleverWild · 1 comment
Member

Background

Currently the codebase uses thiserror-derived enums for error handling. Each module defines a single large enum (e.g. Error in actors/client/auth.rs with 9 variants, ConnectError / ClientSignError in arbiter-client). Individual functions return Result<_, Error> but can only produce 2–3 of those variants in practice. Callers are forced to handle impossible variants, and the type system offers no proof that all reachable error cases are covered.

Problem

  • Functions advertise a superset of errors they can actually emit
  • Match arms contain _ => unreachable!() or silent catch-alls
  • Separate enums (e.g. ConnectError vs ClientSignError) are created purely to scope error variants — a workaround for the lack of per-function precision
  • Adding a new error variant to a shared enum silently widens the contract of every function that returns it

Proposal

Migrate to terrors and replace module-level error enums with OneOf<(ErrA, ErrB, ...)> structural types.

Each function declares exactly the errors it can produce:

// Before
async fn get_nonce(..) -> Result<Option<(i32, i32)>, Error>  // 9 possible variants

// After
async fn get_nonce(..) -> Result<Option<(i32, i32)>, OneOf<(DatabasePoolUnavailable, DatabaseOperationFailed)>>

Errors narrow as they are handled and widen as they propagate up the call stack — both checked at compile time.

Expected outcomes

  • Impossible error variants are eliminated from function signatures
  • Exhaustive match is enforced by the compiler without catch-all arms
  • Error type of a public function becomes self-documenting
  • Separate enums for context scoping are no longer needed

Trade-offs to consider

  • ?-based error propagation requires explicit .broaden() calls instead of From impls — more verbose at call sites
  • Compiler diagnostics for SupersetOf trait violations can be hard to read
  • Integration with tonic, diesel, and other crates that return their own error types needs adapter layer

Scope

  • arbiter-client: replace ConnectError + ClientSignError
  • arbiter-server/actors/client/auth: replace Error
  • arbiter-server/actors/user_agent/auth: replace Error
  • Remaining server actors and context modules
## Background Currently the codebase uses `thiserror`-derived enums for error handling. Each module defines a single large enum (e.g. `Error` in `actors/client/auth.rs` with 9 variants, `ConnectError` / `ClientSignError` in `arbiter-client`). Individual functions return `Result<_, Error>` but can only produce 2–3 of those variants in practice. Callers are forced to handle impossible variants, and the type system offers no proof that all reachable error cases are covered. ## Problem - Functions advertise a superset of errors they can actually emit - Match arms contain `_ => unreachable!()` or silent catch-alls - Separate enums (e.g. `ConnectError` vs `ClientSignError`) are created purely to scope error variants — a workaround for the lack of per-function precision - Adding a new error variant to a shared enum silently widens the contract of every function that returns it ## Proposal Migrate to [`terrors`](https://lib.rs/crates/terrors) and replace module-level error enums with `OneOf<(ErrA, ErrB, ...)>` structural types. Each function declares exactly the errors it can produce: ```rust // Before async fn get_nonce(..) -> Result<Option<(i32, i32)>, Error> // 9 possible variants // After async fn get_nonce(..) -> Result<Option<(i32, i32)>, OneOf<(DatabasePoolUnavailable, DatabaseOperationFailed)>> ``` Errors narrow as they are handled and widen as they propagate up the call stack — both checked at compile time. ## Expected outcomes - Impossible error variants are eliminated from function signatures - Exhaustive match is enforced by the compiler without catch-all arms - Error type of a public function becomes self-documenting - Separate enums for context scoping are no longer needed ## Trade-offs to consider - `?`-based error propagation requires explicit `.broaden()` calls instead of `From` impls — more verbose at call sites - Compiler diagnostics for `SupersetOf` trait violations can be hard to read - Integration with `tonic`, `diesel`, and other crates that return their own error types needs adapter layer ## Scope - [ ] `arbiter-client`: replace `ConnectError` + `ClientSignError` - [ ] `arbiter-server/actors/client/auth`: replace `Error` - [ ] `arbiter-server/actors/user_agent/auth`: replace `Error` - [ ] Remaining server actors and context modules
CleverWild added the
Kind
Enhancement
Compat
Breaking
Reviewed
Pending
4
labels 2026-03-15 17:31:00 +00:00
CleverWild added reference PoC-terrors 2026-03-15 20:12:38 +00:00
CleverWild added
Reviewed
Confirmed
1
and removed
Reviewed
Pending
4
labels 2026-03-17 18:40:00 +00:00
CleverWild added
Reviewed
Won't Fix
3
and removed
Reviewed
Confirmed
1
labels 2026-03-26 18:04:52 +00:00
Author
Member

Closed due to issues with warning unused code and difficulty implementing this with stable rust.

Closed due to issues with warning unused code and difficulty implementing this with stable rust.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: MarketTakers/arbiter#32