-
-
Notifications
You must be signed in to change notification settings - Fork 0
Patterns and troubleshooting
This page collects recipes for common error-handling tasks along with debugging strategies.
Prefer typed conversions over string formatting. masterror exposes helper
constructors and feature-gated conversions.
use masterror::{AppCode, AppError};
pub async fn fetch_user(client: &reqwest::Client) -> masterror::AppResult<String> {
let response = client.get("https://example.com/user").send().await.map_err(|err| {
AppError::external_api("failed to reach user service")
.with_code(AppCode::new("UPSTREAM_HTTP"))
.with_context(err)
})?;
response.text().await.map_err(|err| {
AppError::external_api("failed to decode response body").with_context(err)
})
}Enable the reqwest feature to classify timeouts and HTTP status codes
automatically. Similar conversions exist for sqlx, redis, validator,
config, and more.
Surface validation failures as structured data so clients can highlight fields.
use masterror::{AppCode, AppError};
use serde::Deserialize;
use validator::Validate;
#[derive(Debug, Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 3))]
username: String,
#[validate(email)]
email: String,
}
pub fn validate(payload: &CreateUser) -> masterror::AppResult<()> {
payload.validate().map_err(|err| {
AppError::validation("invalid user payload")
.with_code(AppCode::new("VALIDATION_ERROR"))
.with_details(&err)
})
}validator::ValidationErrors implements Serialize, so it plugs directly into
with_details.
Sometimes you need to control the HTTP layer yourself (e.g., custom middleware).
Convert AppError into ErrorResponse and format it however you need.
fn to_json(err: &masterror::AppError) -> serde_json::Value {
let response: masterror::ErrorResponse = err.clone().into();
serde_json::json!({
"status": response.status.as_u16(),
"code": response.code,
"message": response.message,
"details": response.details,
})
}The clone is cheap because AppError uses shared references for heavy context
objects.
- Log errors at the boundary with
tracing::error!, includingkind,code, andretrymetadata. - Attach upstream errors via
with_context. When you need additional metadata, derive your error type with fields annotated using#[provide]frommasterror::Error.
#[tracing::instrument(skip(err))]
fn log_for_support(err: &masterror::AppError) {
tracing::error!(
kind = ?err.kind,
code = ?err.code,
retry = ?err.retry,
auth = ?err.www_authenticate,
"request failed",
);
}#[tracing::instrument] captures spans automatically, so support teams can
reconstruct what happened.
| Symptom | Checklist |
|---|---|
| Validation failures return HTTP 500 | Enable the validator feature and expose handlers as AppResult<T>. |
JSON response lacks code
|
Call .with_code(AppCode::new("...")) or derive it via #[app_error(code = ...)]. |
| Logs show duplicated errors | Log once per request at the boundary; do not log again inside helpers. |
with_details fails to compile |
Ensure the value implements Serialize (derive or implement it manually). |
| Need to inspect nested errors | Call err.context() to retrieve captured sources, including anyhow::Error. |
- Unit-test constructors: assert on
AppErrorKind,AppCode, retry hints, and JSON serialisation. Useserde_json::to_valuefor comparisons. - Integration-test HTTP handlers: send requests using
axum::Routeroractix_web::test::TestServerand assert on status codes plus JSON bodies. - Property-based tests (
proptest) are great for validating validation logic and parsing code — ensure the error surfaces the expected code even for extreme inputs.
Keep tests deterministic and avoid network calls; use mocks or in-memory services instead.