pleme-error
Unified error handling library for Pleme platform services.
Philosophy
This library implements Railway-Oriented Programming (Scott Wlaschin) principles:
- Errors are first-class citizens
- Two tracks: success path vs error path
- Composable error handling
- Fail-fast with meaningful context
Industry Standards
- Railway-Oriented Programming (Scott Wlaschin) - Functional error handling
- Domain-Driven Design (Eric Evans) - Domain-specific error types
- 12-Factor App - Treat errors as event streams
- Rust Error Handling - thiserror for custom errors, anyhow for context
Features
context- Error context and chaining with anyhowgraphql- GraphQL error conversion for async-graphqlhttp-errors- HTTP status code conversion for Axumlogging- Structured error logging with tracingdatabase- Database error conversions (sqlx, Redis)web- Full web stack (graphql + http-errors + logging)full- All features enabled
Usage
Basic Usage
use ;
Railway-Oriented Programming
use ;
// Success track: User -> Validation -> Persistence -> Email
// Error track: Any error short-circuits to error response
Error Types
// Not found
not_found
// Invalid input
invalid_input
invalid_field
// Database error
db.query.await
.map_err?
// External service error
stripe.charge.await
.map_err?
// Authentication/Authorization
Unauthenticated
PermissionDenied
// Business rules
BusinessRule
// Conflict
Conflict
GraphQL Integration
[]
= { = "../pleme-error", = ["graphql"] }
use ;
use ;
GraphQL error response:
HTTP/Axum Integration
[]
= { = "../pleme-error", = ["http-errors", "serialization"] }
use ;
use ;
async
HTTP error response (404 Not Found):
Database Integration
[]
= { = "../pleme-error", = ["database"] }
use Result;
async
Constraint violations become ServiceError::Conflict:
// Duplicate email → ServiceError::Conflict("Constraint violation: users_email_key")
Structured Logging
[]
= { = "../pleme-error", = ["logging"] }
use ;
Output:
WARN Service error occurred, error="External service error: Stripe - Card declined", context="payment_processing"
Error Context (anyhow pattern)
[]
= { = "../pleme-error", = ["context"] }
use ;
Retry Logic
use ServiceError;
async
Design Principles
1. Fail-Closed Security
Errors default to denial. PermissionDenied and Unauthenticated are explicit.
2. Meaningful Messages
Error messages guide users and operators to solutions.
// ❌ BAD
internal_msg
// ✅ GOOD
database
3. Structured Error Codes
GraphQL and HTTP responses include machine-readable error codes (NOT_FOUND, INVALID_INPUT, etc.)
4. Severity Levels
Errors know if they're severe (database failure) vs expected (not found).
if error.is_severe else
Error Handling Patterns
Pattern 1: Validation Errors
Pattern 2: Not Found vs Empty
// NOT FOUND: Expected resource doesn't exist (404)
db.find_by_id.await
.ok_or_else?
// EMPTY: Query returned no results (200 OK with [])
let users: = db.find_all.await?; // Returns Ok(vec![])
Pattern 3: Business Rule Violations
if order.status == Shipped
Pattern 4: External Service Failures
let payment = stripe_client
.charge
.await
.map_err?;
Migration Guide
From Custom Error Types
// BEFORE
// AFTER
use ;
// Just use ServiceError directly!
From Direct Database Errors
// BEFORE
let user = query_as!
.fetch_one
.await?; // Exposes sqlx::Error to GraphQL/HTTP
// AFTER
use ;
let user = query_as!
.fetch_one
.await?; // Automatically converts to ServiceError with proper HTTP status
Best Practices
- Use descriptive error messages - Help operators debug issues
- Include context - What operation failed? What resource?
- Don't leak sensitive data - No database connection strings, API keys, etc.
- Use proper error types -
NotFoundvsInvalidInputvsInternal - Log severe errors - Database failures, panics, external service outages
- Don't log expected errors - Not found, validation failures
- Add retry logic for retryable errors - Use
is_retryable()
Testing
See Also
- Railway-Oriented Programming - Scott Wlaschin
- Rust Error Handling
- thiserror - Custom error types
- anyhow - Error context and chaining