All fallible SDK APIs use polyester::Result<T> = Result<T, Error>.
For usage patterns see the Error handling guide.
Use err.is_retryable() for backoff classification, err.retry_after() for a server-requested
delay, and err.mutation_outcome_unknown() to identify mutations that require reconciliation
before retrying. These helpers never prove that a mutation was unapplied.
Variants
Error::Auth
Unary unauthenticated / permission deniedError::ApiKeyRejected
Key, signature, or allowlist refusedError::TimestampSkew
Request timestamp outside the skew windowError::SessionRequired
Session-only method called with an API keyError::PermissionDenied
Realtime token HTTP 403 with structured contextError::Validation
Bad input (post_only, ids, enums, shapes, scale)Error::Transport
Network / deadline, usually retryable with backoffError::ResponseContract
Successful RPC returned an invalid response shapeError::RateLimit
Server rate limit or local signing-capacity limitError::Server
Backend 5xx-classError::AdmissionUnavailable
Typed temporary refusal; the order was not admittedError::GatewayTimeout
Gateway timeout; the write may have appliedError::Api
Structured Connect/API errorError::RouteNotFound
Bare HTTP 404: RPC not exposedError::NotImplemented
Server does not offer the callError::Realtime
Realtime connect / decodeError::QueueOverflow
Subscription queue full (fail-closed)Error::RealtimeSetupTimeout
Subscription not confirmed before the deadlineError::SubscriptionEnded
Server ended the subscription and it did not resume
Error::PermissionDenied, Error::ApiKeyRejected, Error::TimestampSkew, and Error::SessionRequired carry optional problem metadata, so include .. in variant patterns. Error::Api carries message, code, metadata, and violations; Error::RateLimit carries message, retry_after, and an optional detail.
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).await {
Ok(r) => println!("{}", r.order_id),
Err(err) if err.mutation_outcome_unknown() => {
// reconcile by client_order_id before deciding whether to resubmit
return Err(err);
}
Err(err) if err.is_retryable() => {
let _ = err.retry_after(); // sleep when present; reconcile this create
}
Err(Error::Validation(msg)) => eprintln!("fix input: {msg}"),
Err(Error::QueueOverflow(_)) => { /* resubscribe, not a unary retry */ }
Err(err) => return Err(err),
}There is no separate catalog-conversion error type like TypeScript. Catalog / precision failures
typically surface as Error::Validation or Error::Api.
Error::ResponseContract is deliberately not retryable: the server returned success, but the
SDK could not safely interpret the response. For an order mutation, mutation_outcome_unknown() is still true because the mutation may have been accepted. Reconcile
by the original stable identifier; do not turn this error into a blind retry loop.
An _OUTCOME_UNKNOWN order code (order_codes::OUTCOME_UNKNOWN) is also not retryable, and its mutation_outcome_unknown() is true: read the order or trigger back first. AUTH_TEMPORARILY_UNAVAILABLE and ERROR_CODE_ACCOUNT_REPAIRING are retryable and changed nothing.
Unary Unauthenticated and PermissionDenied with a typed API-key reason map to Error::ApiKeyRejected, Error::TimestampSkew, or Error::SessionRequired; err.auth_code() returns the typed reason on either transport. Without one they map to Error::Auth. Error::PermissionDenied is not their unary mapping; it carries structured context specifically
for an HTTP 403 while acquiring a private realtime token.
err.is_order_already_terminal() is true for a cancel of an order that has already ended, a repeat
of the cancel that ended it included: treat it as done. err.violations() lists the request fields
an order refusal names. err.existing_order_id() names the order holding a duplicate client order
ID when the server knows it.
MFA helpers
On Error: auth_error_code(), auth_code(), is_mfa_enrollment_required(), is_step_up_required(), is_mfa_elevation_required(), is_mfa_last_factor_required(), for shared
auth codes.
API-key create-key / wallet MFA UX is not part of this SDK (JWT/session only).
Retry sketch
Use is_retryable() for backoff. When mutation_outcome_unknown() is true, reconcile first and
reuse the same client_order_id / request_id / client_trigger_id. Queue overflow is
fail-closed for that subscription.
High-level async calls apply signing backpressure with Tokio-aware waits; they do not block an
executor worker. Direct synchronous Credentials::sign_request returns a retryable rate-limit
error immediately when the bounded timestamp window is full. Async low-level integrations should
use Credentials::sign_request_async.
Class notes
| Concern | Guidance |
|---|---|
| Auth | Fix credentials / Account ID before retrying |
| Validation | Includes post_only on non-limit-GTC |
| Transport / rate limit | Backoff; reconcile when outcome is unknown |
| Response contract | Reconcile; non-retryable without new evidence |
| Queue overflow | Resubscribe; do not treat as unary retry |
| Route not found | Wrong host / incomplete env |