Skip to content

Commit 5eed2a5

Browse files
nolikclaude
andauthored
feat(error): add AppError + anyhow, make startup fail cleanly (#603)
Introduces the error-handling foundation (RECOMMENDATIONS.md §1): - new `src/error.rs` with a `thiserror`-based `AppError` enum (Redis/Postgres/Telegram/Http/Gpt/BadInput), exported from the crate root for use by handler boundaries in a later iteration. - add `thiserror` + `anyhow` dependencies. - `main()` and `run()` now return `anyhow::Result<()>`, and `establish_connection()` returns `anyhow::Result<PgPool>`. Every startup fallible step — Telegram token, Postgres/Redis connect, the three env reads — propagates with `?` + `.context(...)` instead of `unwrap()`/`expect()`. - replace `Bot::from_env()` (panics on missing token) with an explicit `TELOXIDE_TOKEN` read so all boot config fails uniformly. Boot with missing config now prints a contextual error and exits 1 instead of panicking. No behavior change on the happy path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a2f3404 commit 5eed2a5

6 files changed

Lines changed: 59 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ serde = "1.0.228"
2727
serde_json = "1.0"
2828
redis = { version = "0.25.4", features = ["tokio-comp", "connection-manager"] }
2929
html2text = "0.17.1"
30+
thiserror = "2"
31+
anyhow = "1"
3032

3133
[dev-dependencies]
3234
testcontainers = "0.24"

src/boot.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ pub fn build_handler() -> UpdateHandler<RequestError> {
132132
)
133133
}
134134

135-
pub async fn run(deps: AppDeps) {
135+
pub async fn run(deps: AppDeps) -> anyhow::Result<()> {
136136
let AppDeps {
137137
bot,
138138
db_pool,
@@ -149,6 +149,7 @@ pub async fn run(deps: AppDeps) {
149149
.build()
150150
.dispatch()
151151
.await;
152+
Ok(())
152153
}
153154

154155
pub fn message_has_url(regex: &Regex, message_text: &str, text: &MediaText) -> bool {

src/error.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use thiserror::Error;
2+
3+
/// Domain error for rust-bot.
4+
///
5+
/// Low-level failures from the infrastructure the bot talks to (Redis,
6+
/// Postgres, the Telegram Bot API, outbound HTTP) are translated into these
7+
/// variants at the handler boundaries via `#[from]`, so handlers can propagate
8+
/// with `?` and the dispatcher can log a single, typed error.
9+
#[derive(Debug, Error)]
10+
pub enum AppError {
11+
#[error("redis error: {0}")]
12+
Redis(#[from] redis::RedisError),
13+
14+
#[error("postgres error: {0}")]
15+
Postgres(#[from] sqlx::Error),
16+
17+
#[error("telegram error: {0}")]
18+
Telegram(#[from] teloxide::RequestError),
19+
20+
#[error("http error: {0}")]
21+
Http(#[from] reqwest::Error),
22+
23+
#[error("gpt error: {0}")]
24+
Gpt(String),
25+
26+
#[error("bad input: {0}")]
27+
BadInput(String),
28+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod bf_mention_handler;
22
pub mod boot;
33
pub mod chat_gpt_handler;
44
pub mod chat_repository;
5+
pub mod error;
56
pub mod gayness_handler;
67
pub mod gpt_service;
78
pub mod mention_repository;
@@ -12,3 +13,4 @@ pub use boot::{
1213
build_handler, message_has_url, run, AppDeps, GptParameters, MentionParameters,
1314
DEFAULT_OPENAI_BASE_URL,
1415
};
16+
pub use error::AppError;

src/main.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::env;
22
use std::sync::Arc;
33

4+
use anyhow::Context;
45
use log::info;
56
use redis::aio::ConnectionManager;
67
use sqlx::PgPool;
@@ -9,17 +10,21 @@ use teloxide::prelude::*;
910
use rust_bot::{AppDeps, GptParameters, MentionParameters, DEFAULT_OPENAI_BASE_URL};
1011

1112
#[tokio::main]
12-
async fn main() {
13+
async fn main() -> anyhow::Result<()> {
1314
pretty_env_logger::init();
1415
info!("Starting bot...");
1516

16-
let bot = Bot::from_env();
17-
let db_pool = establish_connection().await;
17+
let telegram_token = env::var("TELOXIDE_TOKEN").context("TELOXIDE_TOKEN must be set")?;
18+
let bot = Bot::new(telegram_token);
19+
let db_pool = establish_connection().await?;
1820
let chat_gpt_api_token =
19-
env::var("CHAT_GPT_API_TOKEN").expect("CHAT_GPT_API_TOKEN must be set");
20-
let redis_url = env::var("REDIS_URL").expect("REDIS_URL must be set");
21-
let redis_client = redis::Client::open(redis_url).unwrap();
22-
let redis_connection_manager = ConnectionManager::new(redis_client).await.unwrap();
21+
env::var("CHAT_GPT_API_TOKEN").context("CHAT_GPT_API_TOKEN must be set")?;
22+
let redis_url = env::var("REDIS_URL").context("REDIS_URL must be set")?;
23+
let redis_client =
24+
redis::Client::open(redis_url).context("failed to open Redis client from REDIS_URL")?;
25+
let redis_connection_manager = ConnectionManager::new(redis_client)
26+
.await
27+
.context("failed to connect to Redis")?;
2328

2429
let gpt_parameters = GptParameters {
2530
chat_gpt_api_token: Arc::from(chat_gpt_api_token),
@@ -35,12 +40,12 @@ async fn main() {
3540
mention_parameters: MentionParameters::default(),
3641
};
3742

38-
rust_bot::run(deps).await;
43+
rust_bot::run(deps).await
3944
}
4045

41-
async fn establish_connection() -> PgPool {
42-
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
46+
async fn establish_connection() -> anyhow::Result<PgPool> {
47+
let database_url = env::var("DATABASE_URL").context("DATABASE_URL must be set")?;
4348
PgPool::connect(&database_url)
4449
.await
45-
.expect("Can't establish connection")
50+
.context("failed to connect to Postgres")
4651
}

0 commit comments

Comments
 (0)