Skip to content

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.

ScenarioPatternExample
Input from user/DB/network? + AppErrorDate 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

Replace check-then-unwrap with if let or .filter():

// Before — redundant check + unwrap
let 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 match
if let Some(s) = settings.as_ref().filter(|s| !s.numbers.is_empty()) {
let numbers = &s.numbers;
}

Check that no .unwrap() calls exist outside test modules:

Terminal window
grep -rn '\.unwrap()' src/ | grep -v '#\[cfg(test)\]' | grep -v 'mod tests'