Skip to content

Error handling ​

Every fallible function in botrs returns Result<T, BotError>. BotError is a thiserror enum; you usually pattern-match a few variants and propagate the rest with ?.

Variants you'll actually see ​

The variants you'll match on most often correspond to outcomes the QQ API and gateway produce:

  • BotError::Api { code, message } — QQ-side rejection from any REST call. code is the platform error code; message is the human-readable reason.
  • BotError::AuthenticationFailed(String) — HTTP 401. The Token is wrong or revoked.
  • BotError::Forbidden(String) — HTTP 403. The bot lacks permission for the action.
  • BotError::NotFound(String) — HTTP 404. Targeted resource (channel, message, member) does not exist.
  • BotError::MethodNotAllowed(String) — HTTP 405. Wrong endpoint or method.
  • BotError::Server(String) — HTTP 500 / 504 with the parsed platform diagnostic. Inspect the embedded code/message before deciding whether to retry.
  • BotError::SequenceNumber(String) — reserved for sequence/order errors from the low-level status mapper; normal REST 429 responses are returned as RateLimit.
  • BotError::RateLimit { retry_after } — explicit rate-limit response. retry_after is in seconds.
  • BotError::Auth(String) / BotError::Config(String) / BotError::InvalidData(String) — local validation failures (bad app id, malformed env, payload that can't serialize).
  • BotError::Connection(String) / BotError::Gateway(String) / BotError::Session(String) — gateway lifecycle failures.
  • BotError::Timeout — request didn't complete inside the configured HttpClient timeout.
  • BotError::Http(reqwest::Error) / BotError::WebSocket(...) / BotError::Json(...) / BotError::Url(...) / BotError::Io(...) — transport-level wrappers. These already render usefully with Display, so logging them is usually enough.
  • BotError::Sdk(SdkError) — an internal SDK error carrying a numeric code and trace ID, used by the gateway state machine. Match it for diagnostics, then propagate.

How errors reach you ​

REST calls (BotApi::*) return the error directly. Inside a handler, you decide what to do:

rust
async fn message_create(&self, mut session: ChannelReplySession) {
    let params = MessageParams::new_text("hi");
    if let Err(e) = session.send_message(params).await {
        match e {
            BotError::RateLimit { retry_after } => {
                tracing::warn!(retry_after, "rate limited; dropping reply");
            }
            BotError::Forbidden(_) => {
                tracing::warn!("missing permission for channel");
            }
            other => tracing::error!(error = %other, "send failed"),
        }
    }
}

If dispatch setup fails before your handler is called, the framework calls EventHandler::error(&self, error: BotError) once and then continues consuming events. Payload parse failures are logged and dropped; handler panics are not converted into BotError. Override error to plug in metrics or alerting, but note that the framework does not retry the original event for you.

Retry helpers ​

BotError::is_retryable(&self) returns true for Http (timeouts and connect errors only), WebSocket, Connection, Gateway, Timeout, and RateLimit. BotError::retry_after(&self) returns a suggested delay in seconds (retry_after for rate limits, 5 for Connection, 1 for Gateway, 3 for Timeout, 1 for everything else retryable, None otherwise).

These are advisory. The framework itself does not retry on your behalf — every BotApi call fails immediately on the first error, and gateway reconnects are throttled by Gateway::session_start_interval, not by retry_after. If you want retry semantics around a specific call, write the loop yourself, consulting is_retryable / retry_after:

rust
loop {
    match session.send_message(params.clone()).await {
        Ok(resp) => break Ok(resp),
        Err(e) if e.is_retryable() => {
            if let Some(secs) = e.retry_after() {
                tokio::time::sleep(Duration::from_secs(secs)).await;
                continue;
            }
            break Err(e);
        }
        Err(e) => break Err(e),
    }
}

Keep these loops bounded — there is no protection against an infinite retry inside a handler tying up the dispatch task.

Status code mapping ​

The HTTP layer maps status codes into BotError variants before returning from BotApi calls:

StatusVariant
401AuthenticationFailed
403Forbidden
404NotFound
405MethodNotAllowed
429RateLimit
500 / 504Server
otherApi { code: status, message }

Released under the MIT License.