Error Handling
Every handler returns HandlerResult, which is Result<(), HandlerError>. When a handler returns Err, the application dispatches the error to any registered error handlers instead of crashing.
HandlerError
HandlerError wraps any error type. The Other variant holds a Box<dyn std::error::Error + Send + Sync>:
#![allow(unused)]
fn main() {
use rust_tg_bot::ext::prelude::HandlerError;
// From any std error type
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let e = HandlerError::Other(Box::new(io_err));
}
Inside handlers, the ? operator on Telegram API calls automatically converts errors to HandlerError. For other error types, use .map_err:
#![allow(unused)]
fn main() {
use rust_tg_bot::ext::prelude::{Arc, Context, HandlerError, HandlerResult, Update};
async fn handler(update: Arc<Update>, context: Context) -> HandlerResult {
let chat_id = update.effective_chat().map(|c| c.id).unwrap();
// Telegram errors convert directly with ?
context.reply_text(&update, "Hello!").await?;
// Other errors need .map_err
let _data = std::fs::read_to_string("config.toml")
.map_err(|e| HandlerError::Other(Box::new(e)))?;
Ok(())
}
}
Deliberately Triggering Errors
Sometimes you want to signal an error condition from a handler:
#![allow(unused)]
fn main() {
async fn bad_command(
_update: Arc<Update>,
_context: Context,
) -> HandlerResult {
Err(HandlerError::Other(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"This is a deliberately triggered error from /bad_command!",
))))
}
}
Registering an Error Handler
app.add_error_handler registers a callback that runs whenever any handler returns Err:
#![allow(unused)]
fn main() {
use rust_tg_bot::ext::prelude::{
ApplicationBuilder, Arc, CallbackContext, Context, HandlerError,
HandlerResult, CommandHandler, Update,
};
async fn error_handler(
update: Option<Arc<Update>>,
context: CallbackContext,
) -> bool {
let error_text = context
.error
.as_ref()
.map(|e| format!("{e}"))
.unwrap_or_else(|| "Unknown error".to_string());
tracing::error!("Exception while handling an update: {error_text}");
// Build diagnostic info
let update_str = update
.as_ref()
.map(|u| format!("{u:?}"))
.unwrap_or_else(|| "No update".to_string());
let chat_data_str = context
.chat_data()
.await
.map(|d| format!("{d:?}"))
.unwrap_or_else(|| "None".to_string());
let user_data_str = context
.user_data()
.await
.map(|d| format!("{d:?}"))
.unwrap_or_else(|| "None".to_string());
// Truncate to respect the 4096-char Telegram limit
let message = format!(
"An exception was raised while handling an update\n\n\
update = {update_str}\n\n\
chat_data = {chat_data_str}\n\n\
user_data = {user_data_str}\n\n\
error = {error_text}"
);
let message = if message.len() > 4000 {
format!("{}...(truncated)", &message[..4000])
} else {
message
};
let dev_chat_id: i64 = std::env::var("DEVELOPER_CHAT_ID")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if dev_chat_id != 0 {
let _ = context
.bot()
.send_message(dev_chat_id, &message)
.await;
}
// Return false so other error handlers (if any) can also run
false
}
}
Error Handler Signature
The error handler signature is:
#![allow(unused)]
fn main() {
async fn error_handler(
update: Option<Arc<Update>>,
context: CallbackContext,
) -> bool
}
update– the update that caused the error, if available. It isNonefor errors that occur outside of update processing.context– aCallbackContextthat provides access to the bot, user/chat data, and theerrorfield.- Returns
bool– returnfalseto allow other error handlers to run;trueto stop the chain.
Registering the Handler
Error handlers must be wrapped in an Arc and pinned:
#![allow(unused)]
fn main() {
app.add_error_handler(
Arc::new(|update, ctx| Box::pin(error_handler(update, ctx))),
true, // block: whether to wait for the handler to complete
).await;
}
Accessing Error Details
The error is available on context.error:
#![allow(unused)]
fn main() {
async fn error_handler(
update: Option<Arc<Update>>,
context: CallbackContext,
) -> bool {
if let Some(ref err) = context.error {
tracing::error!("Handler error: {err}");
}
// You can also access data stores for diagnostics
if let Some(user_data) = context.user_data().await {
tracing::debug!("User data at time of error: {user_data:?}");
}
false
}
}
Combining with Typed Errors
Define a domain error type with thiserror for structured error handling:
#![allow(unused)]
fn main() {
use thiserror::Error;
use rust_tg_bot::ext::prelude::HandlerError;
#[derive(Debug, Error)]
pub enum BotError {
#[error("user {user_id} is not authorised for {action}")]
Unauthorised { user_id: i64, action: String },
#[error("database error: {0}")]
Database(String),
#[error("invalid input: {0}")]
InvalidInput(String),
}
// Convert to HandlerError
impl From<BotError> for HandlerError {
fn from(e: BotError) -> Self {
HandlerError::Other(Box::new(e))
}
}
}
Use it in handlers:
#![allow(unused)]
fn main() {
async fn restricted(update: Arc<Update>, context: Context) -> HandlerResult {
let user = update
.effective_user()
.ok_or_else(|| BotError::Unauthorised {
user_id: 0,
action: "restricted".into(),
})?;
if user.id != 123456789 {
return Err(BotError::Unauthorised {
user_id: user.id,
action: "restricted".into(),
}.into());
}
context.reply_text(&update, "Access granted.").await?;
Ok(())
}
}
Best Practices
Always Answer Callback Queries
If your handler for a callback query fails before calling answer_callback_query, the user sees a perpetual loading spinner. Use the error handler to clean up:
#![allow(unused)]
fn main() {
async fn error_handler(
update: Option<Arc<Update>>,
context: CallbackContext,
) -> bool {
// If the errored update was a callback query, answer it
if let Some(ref u) = update {
if let Some(cq) = u.callback_query() {
let _ = context
.bot()
.answer_callback_query(&cq.id)
.await;
}
}
// Log the error
if let Some(ref err) = context.error {
tracing::error!("Handler error: {err}");
}
false
}
}
Avoid unwrap in Handlers
Panics inside handlers are caught by the async runtime, but they produce opaque error messages. Prefer ok_or and ?:
#![allow(unused)]
fn main() {
// Avoid:
let msg = update.effective_message().unwrap();
// Prefer:
let msg = update.effective_message().ok_or_else(|| {
HandlerError::Other("update has no message".into())
})?;
}
Or simply return early:
#![allow(unused)]
fn main() {
let Some(msg) = update.effective_message() else {
return Ok(());
};
}
Complete Example
use rust_tg_bot::ext::prelude::{
ApplicationBuilder, Arc, CallbackContext, CommandHandler, Context,
HandlerError, HandlerResult, Update,
};
async fn start(update: Arc<Update>, context: Context) -> HandlerResult {
let chat_id = update.effective_chat().map(|c| c.id).unwrap_or(0);
context
.reply_text(
&update,
&format!(
"Use /bad_command to cause an error.\nYour chat id is {}.",
chat_id,
),
)
.await?;
Ok(())
}
async fn bad_command(
_update: Arc<Update>,
_context: Context,
) -> HandlerResult {
Err(HandlerError::Other(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"This is a deliberately triggered error!",
))))
}
async fn on_error(
update: Option<Arc<Update>>,
context: CallbackContext,
) -> bool {
let error_text = context
.error
.as_ref()
.map(|e| format!("{e}"))
.unwrap_or_else(|| "Unknown error".to_string());
tracing::error!("Error: {error_text}");
let dev_id: i64 = std::env::var("DEVELOPER_CHAT_ID")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if dev_id != 0 {
let msg = format!("Error: {error_text}");
let _ = context.bot().send_message(dev_id, &msg).await;
}
false
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let token = std::env::var("TELEGRAM_BOT_TOKEN").unwrap();
let app = ApplicationBuilder::new().token(token).build();
app.add_handler(CommandHandler::new("start", start), 0).await;
app.add_handler(CommandHandler::new("bad_command", bad_command), 0).await;
app.add_error_handler(
Arc::new(|update, ctx| Box::pin(on_error(update, ctx))),
true,
).await;
println!("Error handler bot is running. Press Ctrl+C to stop.");
app.run_polling().await.unwrap();
}
Next Steps
- Custom Filters – prevent errors by filtering updates before they reach handlers.
- Testing – assert that your error handler is invoked correctly.