Skip to content

Commit 7fa30ed

Browse files
lklimekclaude
andauthored
refactor: introduce TaskError typed error envelope (#660) (#665)
* refactor: introduce TaskError typed error envelope (#660) Replace `TaskResult::Error(String)` with `TaskResult::Error(TaskError)`. TaskError provides typed error propagation with Display for user-facing messages and Debug for technical details, while From<String> ensures zero-breakage backwards compatibility with all existing code. - Create TaskError enum with Generic(String) + #[from] variants for SpvError, DashPayError, ConfigError, GroveSTARKError, WalletError - Wire TaskResult::Error(TaskError) through app.rs, connection_status.rs, and query_dpns_contested_resources.rs - Change run_backend_task to return Result<_, TaskError> - app.rs handler now shows Display text in banner + Debug in details - Document TaskError pattern in CLAUDE.md - Add manual test scenarios Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: add Other(Box<dyn Error>) variant, dev-only details, transparent errors - Add Other(Box<dyn Error + Send + Sync>) catch-all variant to TaskError - Show Debug details in error banner only in developer mode - Use #[error(transparent)] for proper error chain propagation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: adapt TaskError handler to v1.0-dev MessageBanner API MessageBanner on v1.0-dev takes &str, not impl Display. Convert TaskError to string before passing to set_global and with_details. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * remove testing docs, not applicable here --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0a78539 commit 7fa30ed

7 files changed

Lines changed: 90 additions & 35 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ Screen::ui() → AppAction::BackendTask(task)
122122
123123
**Backend task enums**: `BackendTask` has variants like `IdentityTask(IdentityTask)`, `WalletTask(WalletTask)`, `TokenTask(Box<TokenTask>)`, etc. Each sub-enum has its own variants and corresponding `run_*_task()` method. Results are `BackendTaskSuccessResult` with 50+ typed variants.
124124
125+
**Error handling**: Backend tasks return `Result<T, TaskError>` (`src/backend_task/error.rs`). `TaskError` is a typed error envelope — `Display` produces user-friendly text for `MessageBanner`, `Debug` provides technical details for logs. `From<String>` ensures backwards compatibility: existing `Result<T, String>` code works unchanged. Domain errors (`DashPayError`, `SpvError`, etc.) are wired as `#[from]` variants for automatic conversion via `?`. When adding new backend error types, add a `#[from]` variant to `TaskError` rather than converting to `String`.
126+
125127
## Screen Pattern
126128
127129
All screens implement the `ScreenLike` trait:
@@ -178,7 +180,7 @@ User-facing messages use `MessageBanner` (`src/ui/components/message_banner.rs`)
178180
179181
## Database
180182
181-
Single SQLite connection wrapped in `Mutex<Connection>`. Schema initialized in `database/initialization.rs`. Domain modules provide typed CRUD methods. Backend task errors are `Result<T, String>`string errors display directly to users.
183+
Single SQLite connection wrapped in `Mutex<Connection>`. Schema initialized in `database/initialization.rs`. Domain modules provide typed CRUD methods. Backend task errors use `TaskError` (`src/backend_task/error.rs`)see App Task System section above.
182184
183185
## Platform Targets
184186

src/app.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::app_dir::app_user_data_file_path;
33
use crate::app_dir::{copy_env_file_if_not_exists, create_app_user_data_directory_if_not_exists};
44
use crate::backend_task::contested_names::ContestedResourceTask;
55
use crate::backend_task::core::CoreItem;
6+
use crate::backend_task::error::TaskError;
67
use crate::backend_task::{BackendTask, BackendTaskSuccessResult};
78
use crate::components::core_zmq_listener::{CoreZMQListener, ZMQMessage};
89
use crate::context::AppContext;
@@ -50,11 +51,11 @@ use tokio::sync::mpsc as tokiompsc;
5051
pub enum TaskResult {
5152
Refresh,
5253
Success(Box<BackendTaskSuccessResult>),
53-
Error(String),
54+
Error(TaskError),
5455
}
5556

56-
impl From<Result<BackendTaskSuccessResult, String>> for TaskResult {
57-
fn from(value: Result<BackendTaskSuccessResult, String>) -> Self {
57+
impl From<Result<BackendTaskSuccessResult, TaskError>> for TaskResult {
58+
fn from(value: Result<BackendTaskSuccessResult, TaskError>) -> Self {
5859
match value {
5960
Ok(value) => TaskResult::Success(Box::new(value)),
6061
Err(e) => TaskResult::Error(e),
@@ -983,10 +984,14 @@ impl App for AppState {
983984
}
984985
}
985986
}
986-
TaskResult::Error(message) => {
987-
MessageBanner::set_global(ctx, &message, MessageType::Error);
987+
TaskResult::Error(err) => {
988+
let msg = err.to_string();
989+
let handle = MessageBanner::set_global(ctx, &msg, MessageType::Error);
990+
if self.current_app_context().is_developer_mode() {
991+
handle.with_details(&format!("{err:?}"));
992+
}
988993
self.visible_screen_mut()
989-
.display_message(&message, MessageType::Error);
994+
.display_message(&msg, MessageType::Error);
990995
}
991996
TaskResult::Refresh => {
992997
self.visible_screen_mut().refresh();

src/backend_task/contested_names/query_dpns_contested_resources.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ impl AppContext {
197197
}
198198
Err(e) => {
199199
tracing::error!("Error querying dpns end times: {}", e);
200-
if let Err(send_err) = sender.send(TaskResult::Error(e)).await {
200+
if let Err(send_err) = sender.send(TaskResult::Error(e.into())).await {
201201
tracing::warn!(
202202
"Failed to send error for dpns end times query: {}",
203203
send_err
@@ -249,7 +249,7 @@ impl AppContext {
249249
}
250250
Err(e) => {
251251
tracing::error!("Error querying dpns vote contenders for {}: {}", name, e);
252-
if let Err(send_err) = sender.send(TaskResult::Error(e)).await {
252+
if let Err(send_err) = sender.send(TaskResult::Error(e.into())).await {
253253
tracing::warn!(
254254
"Failed to send error for vote contenders query for {}: {}",
255255
name,

src/backend_task/error.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Typed error envelope for backend tasks.
2+
//!
3+
//! `Display` → user-friendly text (shown in `MessageBanner`).
4+
//! `Debug` → variant name + fields (logged and shown in collapsible details).
5+
//! `From<String>` → backwards compatible with existing `Result<T, String>` code.
6+
7+
use thiserror::Error;
8+
9+
/// App-level error envelope for backend tasks.
10+
#[derive(Debug, Error)]
11+
pub enum TaskError {
12+
/// Legacy string error — backwards compatible with all existing code.
13+
#[error("{0}")]
14+
Generic(String),
15+
16+
/// Boxed error — catch-all for errors without a dedicated variant.
17+
#[error(transparent)]
18+
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
19+
20+
/// SPV subsystem errors.
21+
#[error(transparent)]
22+
Spv(#[from] crate::spv::SpvError),
23+
24+
/// DashPay domain errors.
25+
#[error(transparent)]
26+
DashPay(#[from] crate::backend_task::dashpay::errors::DashPayError),
27+
28+
/// Configuration errors.
29+
#[error(transparent)]
30+
Config(#[from] crate::config::ConfigError),
31+
32+
/// GroveSTARK prover errors.
33+
#[error(transparent)]
34+
GroveStark(#[from] crate::model::grovestark_prover::GroveSTARKError),
35+
36+
/// Wallet errors.
37+
#[error(transparent)]
38+
Wallet(#[from] crate::database::WalletError),
39+
}
40+
41+
impl From<String> for TaskError {
42+
fn from(s: String) -> Self {
43+
TaskError::Generic(s)
44+
}
45+
}

src/backend_task/mod.rs

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::app::TaskResult;
2+
use crate::backend_task::error::TaskError;
23
use crate::backend_task::contested_names::ContestedResourceTask;
34
use crate::backend_task::contract::ContractTask;
45
use crate::backend_task::core::{CoreItem, CoreTask};
@@ -46,6 +47,7 @@ pub mod contract;
4647
pub mod core;
4748
pub mod dashpay;
4849
pub mod document;
50+
pub mod error;
4951
pub mod grovestark;
5052
pub mod identity;
5153
pub mod mnlist;
@@ -279,7 +281,7 @@ impl AppContext {
279281
self: &Arc<Self>,
280282
tasks: Vec<BackendTask>,
281283
sender: SenderAsync<TaskResult>,
282-
) -> Vec<Result<BackendTaskSuccessResult, String>> {
284+
) -> Vec<Result<BackendTaskSuccessResult, TaskError>> {
283285
let mut results = Vec::new();
284286
for task in tasks {
285287
match self.run_backend_task(task, sender.clone()).await {
@@ -295,7 +297,7 @@ impl AppContext {
295297
self: &Arc<Self>,
296298
tasks: Vec<BackendTask>,
297299
sender: SenderAsync<TaskResult>,
298-
) -> Vec<Result<BackendTaskSuccessResult, String>> {
300+
) -> Vec<Result<BackendTaskSuccessResult, TaskError>> {
299301
let futures = tasks
300302
.into_iter()
301303
.map(|task| {
@@ -313,44 +315,44 @@ impl AppContext {
313315
self: &Arc<Self>,
314316
task: BackendTask,
315317
sender: SenderAsync<TaskResult>,
316-
) -> Result<BackendTaskSuccessResult, String> {
318+
) -> Result<BackendTaskSuccessResult, TaskError> {
317319
let sdk = self.sdk.load().as_ref().clone();
318320
match task {
319321
BackendTask::ContractTask(contract_task) => {
320-
self.run_contract_task(*contract_task, &sdk, sender).await
321-
}
322-
BackendTask::ContestedResourceTask(contested_resource_task) => {
323-
self.run_contested_resource_task(contested_resource_task, &sdk, sender)
324-
.await
322+
Ok(self.run_contract_task(*contract_task, &sdk, sender).await?)
325323
}
324+
BackendTask::ContestedResourceTask(contested_resource_task) => Ok(self
325+
.run_contested_resource_task(contested_resource_task, &sdk, sender)
326+
.await?),
326327
BackendTask::IdentityTask(identity_task) => {
327-
self.run_identity_task(identity_task, &sdk, sender).await
328+
Ok(self.run_identity_task(identity_task, &sdk, sender).await?)
328329
}
329330
BackendTask::DocumentTask(document_task) => {
330-
self.run_document_task(*document_task, &sdk).await
331+
Ok(self.run_document_task(*document_task, &sdk).await?)
331332
}
332-
BackendTask::CoreTask(core_task) => self.run_core_task(core_task).await,
333+
BackendTask::CoreTask(core_task) => Ok(self.run_core_task(core_task).await?),
333334
BackendTask::DashPayTask(dashpay_task) => {
334-
self.run_dashpay_task(*dashpay_task, &sdk).await
335-
}
336-
BackendTask::BroadcastStateTransition(state_transition) => {
337-
self.broadcast_state_transition(state_transition, &sdk)
338-
.await
335+
Ok(self.run_dashpay_task(*dashpay_task, &sdk).await?)
339336
}
337+
BackendTask::BroadcastStateTransition(state_transition) => Ok(self
338+
.broadcast_state_transition(state_transition, &sdk)
339+
.await?),
340340
BackendTask::TokenTask(token_task) => {
341-
self.run_token_task(*token_task, &sdk, sender).await
341+
Ok(self.run_token_task(*token_task, &sdk, sender).await?)
342342
}
343-
BackendTask::SystemTask(system_task) => self.run_system_task(system_task, sender).await,
344-
BackendTask::MnListTask(mnlist_task) => {
345-
mnlist::run_mnlist_task(self, mnlist_task).await
343+
BackendTask::SystemTask(system_task) => {
344+
Ok(self.run_system_task(system_task, sender).await?)
346345
}
347-
BackendTask::PlatformInfo(platform_info_task) => {
348-
self.run_platform_info_task(platform_info_task, &sdk).await
346+
BackendTask::MnListTask(mnlist_task) => {
347+
Ok(mnlist::run_mnlist_task(self, mnlist_task).await?)
349348
}
349+
BackendTask::PlatformInfo(platform_info_task) => Ok(self
350+
.run_platform_info_task(platform_info_task, &sdk)
351+
.await?),
350352
BackendTask::GroveSTARKTask(grovestark_task) => {
351-
grovestark::run_grovestark_task(grovestark_task, &sdk).await
353+
Ok(grovestark::run_grovestark_task(grovestark_task, &sdk).await?)
352354
}
353-
BackendTask::WalletTask(wallet_task) => self.run_wallet_task(wallet_task).await,
355+
BackendTask::WalletTask(wallet_task) => Ok(self.run_wallet_task(wallet_task).await?),
354356
BackendTask::None => Ok(BackendTaskSuccessResult::None),
355357
}
356358
}

src/context/connection_status.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,8 @@ impl ConnectionStatus {
321321
}
322322
_ => {}
323323
},
324-
TaskResult::Error(message) => {
325-
if message.contains(
324+
TaskResult::Error(err) => {
325+
if err.to_string().contains(
326326
"Failed to get best chain lock for mainnet, testnet, devnet, and local",
327327
) {
328328
self.set_rpc_online(false);

src/database/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod tokens;
1515
mod top_ups;
1616
mod utxo;
1717
mod wallet;
18+
pub use wallet::WalletError;
1819

1920
use dash_sdk::dpp::dashcore::Network;
2021
use rusqlite::{Connection, Params};

0 commit comments

Comments
 (0)