| name | error-handling |
| description | Use when adding new error variants, propagating errors between service layers, or working with the thiserror error hierarchy and the mark_error pattern. |
Error Handling Architecture
Overview
rs-summarizer uses a layered error hierarchy built with thiserror. Each service layer has its own error enum, and ProcessError at the task orchestration layer wraps them all via #[from] conversions.
Error Hierarchy
ProcessError (tasks.rs orchestrator)
├── Transcript(TranscriptError) — via #[from]
├── Summary(SummaryError) — via #[from]
├── Embedding(EmbeddingError) — via #[from]
├── Database(sqlx::Error) — via #[from]
├── RowNotFound — wait_until_row_exists timeout
├── TranscriptTooShort — < 30 words
└── TranscriptTooLong(usize) — > 280,000 words
Service-Level Errors
TranscriptError (yt-dlp / download layer)
pub enum TranscriptError {
InvalidUrl(String),
NoSubtitles,
YtDlpFailed(String),
Timeout(u64),
ParseError(String),
}
SummaryError (Gemini generation layer)
pub enum SummaryError {
ApiError(String),
RateLimited,
TranscriptTooShort,
TranscriptTooLong(usize, usize),
}
EmbeddingError (embedding layer)
pub enum EmbeddingError {
ApiError(String),
EmptyText,
DbError(sqlx::Error),
}
Error Propagation Pattern
In tasks.rs, the ? operator auto-converts service errors into ProcessError:
async fn process_summary_inner(db: &SqlitePool, id: i64, app: &AppState) -> Result<(), ProcessError> {
let transcript = transcript_svc.download(&url).await?;
let result = summary_svc.generate(&text, &model).await?;
embedding_svc.embed_text(&text).await?;
Ok(())
}
The mark_error Pattern
The outer process_summary() function catches ALL errors and stores them in the DB so the frontend can display them:
pub async fn process_summary(db_pool: SqlitePool, identifier: i64, app: AppState) {
if let Err(e) = process_summary_inner(&db_pool, identifier, &app).await {
mark_error(&db_pool, identifier, &e.to_string()).await;
}
}
async fn mark_error(db_pool: &SqlitePool, identifier: i64, error_msg: &str) {
let _ = db::update_summary_chunk(db_pool, identifier, error_msg).await;
let _ = db::mark_summary_done(db_pool, identifier, 0, 0, 0.0, "").await;
}
Critical invariant: mark_error always sets summary_done = true, which stops HTMX polling. Without this, errors would cause infinite polling.
Adding a New Error Variant
- Add the variant to the appropriate enum in
src/errors.rs
- Include a
#[error("...")] message (this becomes the user-visible text via mark_error)
- If the new error comes from an external crate, add
#[from] for automatic conversion
- If it needs to propagate to
ProcessError, add a wrapping variant there too
Route-Level Error Handling
Route handlers don't use the error hierarchy — they return Html<String> directly:
let model = match model {
Some(m) => m.clone(),
None => return Html("<p>Invalid model selected.</p>".to_string()),
};
Only the background task uses the full error chain.
Relevant Files
src/errors.rs — All error enum definitions
src/tasks.rs — process_summary(), mark_error(), error propagation
src/services/transcript.rs — Returns TranscriptError
src/services/summary.rs — Returns SummaryError
src/services/embedding.rs — Returns EmbeddingError