Error Handling Patterns
Loquent bans .unwrap() in production server code. Every fallible call uses proper error propagation or an explicit .expect() with a justification. This guide covers the three patterns used across the codebase.
Pattern 1 — Propagate with ? and AppError
Section titled “Pattern 1 — Propagate with ? and AppError”Use ? with AppError variants for operations that can genuinely fail at runtime. The AppError enum implements IntoResponse, so errors automatically become proper HTTP responses.
use crate::bases::error::AppError;
let cursor = NaiveDate::from_ymd_opt(year, month, 1) .ok_or(AppError::Internal("Invalid date computation".into()))?;Apply this to:
- Date/time arithmetic where inputs come from user data or database values
- Network calls (
TcpListener::bind,axum::serve) - Router initialization (
create_router().await?)
In main.rs, the production launcher propagates errors through the block_on closure:
tokio::runtime::Runtime::new() .expect("Failed to create Tokio runtime") .block_on(async move { let listener = tokio::net::TcpListener::bind(address).await?; let router = create_router().await?; axum::serve(listener, router) .with_graceful_shutdown(shutdown_signal()) .await?; Ok::<(), Box<dyn std::error::Error>>(()) })?;Pattern 2 — .expect() for provably infallible calls
Section titled “Pattern 2 — .expect() for provably infallible calls”When a call returns Option or Result but the inputs are hardcoded constants that always succeed, use .expect() with a BUG: prefix explaining why it cannot fail:
let midnight = NaiveTime::from_hms_opt(0, 0, 0) .expect("BUG: midnight 0:0:0 is always valid");let address = "0.0.0.0".parse() .expect("BUG: hardcoded IP literal is always valid");The BUG: prefix signals to future developers that a panic here means a logic error in the code, not a runtime failure.
Pattern 3 — .expect() for Response::builder().body()
Section titled “Pattern 3 — .expect() for Response::builder().body()”Axum’s Response::builder().body() returns Result, but it only fails if you set an invalid status code or incompatible body type. With valid constants, use a standard .expect() message:
Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from("Invalid call_sid")) .expect("infallible: valid status code and body type")This pattern appears in all IntoResponse implementations — auth response types, phone types, Twilio webhooks, and recording endpoints.
Choosing the right pattern
Section titled “Choosing the right pattern”| Scenario | Pattern | Example |
|---|---|---|
| Input from user/DB/network | ? + AppError | Date parsing, TCP bind |
| Hardcoded constant that always succeeds | .expect("BUG: ...") | NaiveTime(0,0,0), IP literals |
Response::builder().body() with valid args | .expect("infallible: ...") | All IntoResponse impls |
| Test code only | .unwrap() allowed | #[cfg(test)] modules |
Eliminating if x.is_some() { x.unwrap() }
Section titled “Eliminating if x.is_some() { x.unwrap() }”Replace check-then-unwrap with if let or .filter():
// Before — redundant check + unwraplet has_numbers = settings.as_ref().is_some_and(|s| !s.numbers.is_empty());if has_numbers { let numbers = &settings.as_ref().unwrap().numbers;}
// After — single pattern matchif let Some(s) = settings.as_ref().filter(|s| !s.numbers.is_empty()) { let numbers = &s.numbers;}Verification
Section titled “Verification”Check that no .unwrap() calls exist outside test modules:
grep -rn '\.unwrap()' src/ | grep -v '#\[cfg(test)\]' | grep -v 'mod tests'