Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Error handling

What this teaches: the two error shapes in dango-sdkanyhow::Error for one-shot calls, WsError inside subscription items — and how to recover from each.

Mental model

dango-sdk returns anyhow::Result<T> (Result<T, anyhow::Error>) from every fallible async or sync method. The trait impls on HttpClient set type Error = anyhow::Error, so the same pattern propagates from query_app, broadcast_tx, and friends.

The single exception is subscriptions. A SubscriptionStream<T> yields Result<Response<T>, WsError> — a typed enum because callers need to discriminate between "decode failure" and "server-side subscription error".

anyhow::Error: when and how to inspect

Use ? to propagate. When you need to react to a specific failure, downcast:

use {
    anyhow::Result,
    dango_sdk::HttpClient,
    grug::QueryClient,
};
 
async fn try_query(http: &HttpClient) -> Result<()> {
    match http.simulate(my_unsigned_tx()).await {
        Ok(outcome) => println!("ok: gas={}", outcome.gas_used),
        Err(err) => {
            // Drill into the source chain for a specific cause type.
            if let Some(req_err) = err.downcast_ref::<reqwest::Error>() {
                eprintln!("transport: {req_err}");
            } else {
                eprintln!("other: {err:#}");
            }
        }
    }
    Ok(())
}
// fn my_unsigned_tx() -> grug::UnsignedTx { ... }

The :# format spec prints the full cause chain. Prefer tracing::error!(?err) in production.

Common error sources

WhereWhat you'll see
HttpClient::newURL parse error (url::ParseError).
query_app, simulate, broadcast_tx, paginate_*reqwest::Error for network failure; anyhow!("no data returned …") when GraphQL returned errors and no data; deserialization errors from serde_json.
query_block (REST)reqwest::Error plus HTTP status (error_for_status formats body into the message).
SingleSigner::new_first_address_availableanyhow!("no user index found for key hash …") when the factory returns nothing.
Keystore::from_fileI/O, JSON parse, or AES-GCM decryption failures.
WsClient::connectInvalid URL scheme: …, handshake send error, unexpected pre-connection_ack frame.

WsError: subscription items

SubscriptionStream<T> items are Result<graphql_client::Response<T>, WsError>:

use {
    dango_sdk::WsError,
    futures::StreamExt,
};
 
while let Some(item) = stream.next().await {
    match item {
        Ok(resp) => {
            if let Some(errors) = resp.errors {
                eprintln!("graphql payload errors: {errors:?}");
            }
            if let Some(data) = resp.data {
                /* handle data */
            }
        }
        Err(WsError::Closed(reason)) => {
            eprintln!("ws closed: {reason}");
            break;
        }
        Err(WsError::Transport(msg)) => {
            eprintln!("transport: {msg}");
            break;
        }
        Err(WsError::Subscription(payload)) => {
            eprintln!("subscription error: {payload}");
            // Server-side error scoped to this subscription;
            // the stream is over but the connection may still be alive.
        }
        Err(WsError::Decode(msg)) => {
            eprintln!("decode: {msg}");
        }
    }
}
// Ok::<(), anyhow::Error>(())

WsError::Closed and WsError::Transport terminate the stream. WsError::Subscription is server-side: the stream ends but the parent Session keeps running for other subscriptions. WsError::Decode is recoverable in principle but means the schema and the client disagree.

Retry policy

The SDK does not retry. Wrap calls with tokio-retry, backon, or a hand-rolled loop:

use {
    anyhow::Result,
    dango_sdk::HttpClient,
    grug::SearchTxClient,
    std::time::Duration,
    tokio::time::sleep,
};
 
async fn wait_for(http: &HttpClient, hash: grug::Hash256) -> Result<()> {
    for attempt in 0..40 {
        match http.search_tx(hash).await {
            Ok(_) => return Ok(()),
            Err(err) if attempt < 39 => {
                eprintln!("waiting (attempt {attempt}): {err}");
                sleep(Duration::from_millis(500)).await;
            }
            Err(err) => return Err(err),
        }
    }
    unreachable!()
}

Pick a bound that matches the operation. Don't retry forever — backoff is the caller's responsibility.

Next