A lightweight, zero-allocation retry macro for recoverable error handling in async Rust.
retriage handles retry logic and error classification only.
The following concerns are intentionally out of scope:
- Rate limiting / traffic shaping — use
governoror similar - Circuit breaking — use
failsafeortower's circuit breaker - Timeout management — wrap your future with
tokio::time::timeout - Logging — use
tracingor similar
If you need rate limiting alongside retries, manage it inside your closure:
let limiter = Arc::new(RateLimiter::direct(Quota::per_second(10)));
retry!(
{
limiter.until_ready().await; // retriage `does not` manage rate limiting
foo().await
},
policy
).awaitFor logging, emit log events directly within ErrorHandler::handle:
impl ErrorHandler for MyPolicy {
type Err = anyhow::Error;
fn handle<'a>(
&self,
e: &'a Self::Err,
attempt: u32,
backoff: Duration,
) -> ErrorDecision<'a, Self::Err> {
tracing::warn!(attempt, ?backoff, %e, "Retrying operation"); // retriage `does not` manage logging
ErrorDecision::RetryAfter(backoff)
}
}retriage uses tokio::time::sleep internally and requires a Tokio runtime.
[dependencies]
tokio = { version = "1", features = ["time"] }ErrorHandler::Err accepts any Send + 'static type.
| Error type | handle |
dispatch! |
|---|---|---|
anyhow::Error |
✓ | ✓ |
thiserror enum |
✓ use match directly |
— not needed |
Box<dyn std::error::Error> |
✓ | ✓ |
dyn std::error::Error (trait object) |
✓ (due to ?Sized) |
✓ |
std::error::Error is intentionally absent from the bound so that non-std::error::Error
types like anyhow::Error can be used directly. The ?Sized bound enables handling
unsized trait objects (dyn std::error::Error) transparently.
RetryConfigBuilder is not const, so if you need a single config
shared across your application, use std::sync::LazyLock to avoid
rebuilding it on every call:
use std::sync::LazyLock;
use retriage::{
ExponentialConfig, RetryConfigBuilder,
backoff::{Exponential, FullJitter},
retry,
};
use std::time::Duration;
// Note: `static` requires fully explicit type parameters — `_` is not allowed.
static RETRY_CONFIG: LazyLock<ExponentialConfig<MyPolicy, FullJitter>> =
LazyLock::new(|| {
let backoff = Exponential::with_jitter(
Duration::from_millis(100),
Duration::from_millis(1650),
FullJitter,
);
RetryConfigBuilder::new()
.max_retries(4)
.backoff(backoff)
.handler(MyPolicy)
.build()
});
// elsewhere
let foo = retry!({ bar().await }, &*RETRY_CONFIG).await?;Automatic error type coercion — when a block returns a different error
type than H::Err, use the cast parameter to unify them on the stack
without heap allocation:
retry!({ foo().await }, config, |e| e as &DynError).await?;
retry!({ foo().await }, config, |e| e.as_ref()).await?;Full automatic coercion (without the cast parameter) is a planned feature.