# 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. QQ-side outage; transient.
- `BotError::SequenceNumber(String)` — HTTP 429-shaped sequence error. Distinct from `RateLimit` below; usually means message ordering, not throttling.
- `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(Err)` — 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, ctx: Context, msg: Message) {
let params = MessageParams::new_text("hi");
if let Err(e) = ctx.api.post_message_with_params(&ctx.token, "channel", 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 event dispatch itself fails (a malformed payload, a panic in your handler converted to error, etc.), the framework calls `EventHandler::error(&self, error: BotError)` once and then continues consuming events. The default implementation logs with `error!`. Override it to plug in metrics, alerting, or your own panic handling — 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 ctx.api.post_message_with_params(&ctx.token, channel, 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
`http_error_from_status(status, message)` is the conversion the HTTP layer applies internally. It's exposed if you build your own clients on top of `HttpClient`:
| 401 | `AuthenticationFailed` |
| 403 | `Forbidden` |
| 404 | `NotFound` |
| 405 | `MethodNotAllowed` |
| 429 | `SequenceNumber` |
| 500 / 504 | `Server` |
| other | `Api { code: status, message }` |