Match on polyester::Error when you need variant-specific fields. Full variant list: Errors reference.
For retry loops, prefer the classifiers over string matching or hand-rolled variant lists:
err.is_retryable()- transport, rate-limit, and server failures that may succeed after backofferr.retry_after()- honor a server-requested delay when presenterr.mutation_outcome_unknown()- the mutation may already have committed; reconcile before retrying and reuse the original request identity
is_retryable() is not proof that a mutation was unapplied. Only mutation_outcome_unknown() obliges reconciliation and key reuse. These classifiers are
independent: Error::ResponseContract is non-retryable but has an unknown mutation outcome because
the RPC succeeded and only the returned payload violated the SDK contract. An _OUTCOME_UNKNOWN order code is also non-retryable with an unknown outcome: the mutation may still apply.
use polyester::models::{CreateOrderParams, CreateOrderType, CreateSide};
use polyester::{Error, Price, Quantity};
let quantity_scale = client
.catalogs
.base_quantity_scale_for_symbol("BTC-USDT")
.ok_or_else(|| Error::validation("BTC-USDT quantity scale is unavailable"))?;
let mut params = CreateOrderParams::new("BTC-USDT", CreateSide::Buy, CreateOrderType::Limit);
params.quantity = Some(Quantity::from_decimal_str("0.01", quantity_scale, Some("BTC-USDT".into()), None)?);
params.price = Some(Price::from_decimal_str("64000", Some("BTC-USDT".into()))?);
params.client_order_id = Some("mm-bot-001".into());
match client.orders.create(params.clone()).await {
Ok(r) => println!("{}", r.order_id),
Err(err) if err.mutation_outcome_unknown() => {
// Reconcile by client_order_id before deciding whether to retry.
// Retained reuse conflicts; it does not replay the earlier result.
return Err(err);
}
Err(err) if err.is_retryable() => {
if let Some(seconds) = err.retry_after() {
tokio::time::sleep(std::time::Duration::from_secs_f64(seconds)).await;
}
// Reconcile this create by client_order_id before resubmitting.
return Err(err);
}
Err(Error::QueueOverflow(_)) => {
// subscription only, resubscribe after catching up
}
Err(Error::Auth(message)) => {
// Unary unauthenticated and permission_denied without a typed API-key reason.
eprintln!("fix credentials or API-key policy: {message}");
}
Err(Error::ApiKeyRejected { code, .. }) => {
// SIGNATURE_INVALID, KEY_DISABLED, IP_NOT_ALLOWED, ...
eprintln!("fix the key, signature, or allowlist: {code}");
}
Err(Error::Validation(msg)) => {
// fix input (e.g. invalid client_order_id charset/length)
eprintln!("{msg}");
}
Err(err) => return Err(err),
}Retry rules
- Retry when
is_retryable()is true, with exponential backoff. - Local signing-capacity exhaustion is also
Error::RateLimit. Async client calls wait without blocking Tokio worker threads; respectretry_after()if the bounded wait is exhausted. - When
mutation_outcome_unknown()is true, reconcile server state first, then reuse the sameclient_order_id,request_id, orclient_trigger_id. - Do not blindly retry
Error::ResponseContract; reconcile and inspect/escalate the malformed successful response before deciding on any new action. - Reconcile every batch item before retrying;
request_idis not a whole-batch atomicity guarantee. - Do not generate a fresh idempotency key inside the retry loop.
- Realtime overflow is fail-closed: treat it as fatal for that subscription.
Error::TimestampSkewis retryable: the request did nothing, and a retry signs it again. If it persists, fix the local clock.Error::ApiKeyRejected(SIGNATURE_INVALID,KEY_DISABLED,IP_NOT_ALLOWED, ...) andError::SessionRequired(a session-only method called with an API key) are not retryable.err.auth_code()returns the typed reason.- Other unary
UnauthenticatedandPermissionDeniedresponses map toError::Auth; fix the credentials or API-key policy before retrying.Error::PermissionDeniedis reserved for an HTTP 403 from private realtime-token acquisition, wherecode,context, andendpointidentify the denied stream permission. err.is_order_already_terminal()means a cancel found the order already ended. Treat it as done and read the order for how it ended.err.violations()lists the request fields an order refusal names. Branch oncode, then onrule_id.
client_order_id can double-place an order if the first attempt already applied.
Keep the original ID for reconciliation; a duplicate conflict does not replay the earlier outcome.Validation & precision
Local validation failures (Error::Validation) include catalog scale misses, invalid client_order_id / request_id charset or length, and unsupported order controls. Fix the input;
do not retry unchanged.