Skip to main content

qubit_batch/execute/
batch_task_error.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::{
11    any::Any,
12    error::Error,
13    fmt,
14};
15
16/// Error recorded for one task inside a batch execution.
17///
18/// Use this type to distinguish a task's returned business error from a panic
19/// captured while running that task.
20///
21/// ```rust
22/// use qubit_batch::BatchTaskError;
23///
24/// let failed = BatchTaskError::Failed("invalid record");
25/// assert!(failed.is_failed());
26/// assert_eq!(failed.panic_message(), None);
27///
28/// let panicked: BatchTaskError<&'static str> = BatchTaskError::panicked("boom");
29/// assert!(panicked.is_panicked());
30/// assert_eq!(panicked.panic_message(), Some("boom"));
31/// ```
32///
33/// # Type Parameters
34///
35/// * `E` - The task-specific error type.
36///
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum BatchTaskError<E> {
39    /// The task returned its own business error.
40    Failed(E),
41
42    /// The task panicked while running.
43    Panicked {
44        /// Captured panic message when the panic payload is a string.
45        message: Option<String>,
46    },
47}
48
49impl<E> BatchTaskError<E> {
50    /// Creates a panicked task error from a captured panic payload.
51    ///
52    /// # Parameters
53    ///
54    /// * `payload` - Panic payload captured by `catch_unwind`.
55    ///
56    /// # Returns
57    ///
58    /// A panicked task error containing a string message when the payload carries
59    /// one.
60    #[inline]
61    pub fn from_panic_payload(payload: &(dyn Any + Send)) -> Self {
62        match panic_payload_message(payload) {
63            Some(message) => Self::panicked(message),
64            None => Self::panicked_without_message(),
65        }
66    }
67
68    /// Creates a panicked task error with a captured message.
69    ///
70    /// # Parameters
71    ///
72    /// * `message` - Panic message captured from the task.
73    ///
74    /// # Returns
75    ///
76    /// A panicked task error containing `message`.
77    #[inline]
78    pub fn panicked(message: impl Into<String>) -> Self {
79        Self::Panicked {
80            message: Some(message.into()),
81        }
82    }
83
84    /// Creates a panicked task error without a readable message.
85    ///
86    /// # Returns
87    ///
88    /// A panicked task error with no captured message.
89    #[inline]
90    pub const fn panicked_without_message() -> Self {
91        Self::Panicked { message: None }
92    }
93
94    /// Returns whether this task error wraps the task's own error value.
95    ///
96    /// # Returns
97    ///
98    /// `true` if this error is [`Self::Failed`].
99    #[inline]
100    pub const fn is_failed(&self) -> bool {
101        matches!(self, Self::Failed(_))
102    }
103
104    /// Returns whether this task error represents a panic.
105    ///
106    /// # Returns
107    ///
108    /// `true` if this error is [`Self::Panicked`].
109    #[inline]
110    pub const fn is_panicked(&self) -> bool {
111        matches!(self, Self::Panicked { .. })
112    }
113
114    /// Returns the captured panic message, if one is available.
115    ///
116    /// # Returns
117    ///
118    /// `Some(message)` when the panic payload was a string, or `None` for
119    /// business errors and non-string panic payloads.
120    #[inline]
121    pub fn panic_message(&self) -> Option<&str> {
122        match self {
123            Self::Failed(_) | Self::Panicked { message: None } => None,
124            Self::Panicked {
125                message: Some(message),
126            } => Some(message.as_str()),
127        }
128    }
129}
130
131impl<E> fmt::Display for BatchTaskError<E>
132where
133    E: fmt::Display,
134{
135    /// Formats this batch task error for users.
136    ///
137    /// # Parameters
138    ///
139    /// * `f` - Formatter receiving the human-readable message.
140    ///
141    /// # Returns
142    ///
143    /// The formatting result produced by `write!`.
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Self::Failed(error) => write!(f, "task failed: {error}"),
147            Self::Panicked { message: None } => f.write_str("task panicked"),
148            Self::Panicked {
149                message: Some(message),
150            } => write!(f, "task panicked: {message}"),
151        }
152    }
153}
154
155impl<E> Error for BatchTaskError<E>
156where
157    E: Error + 'static,
158{
159    /// Returns the wrapped task error as the source when this error represents
160    /// a business failure.
161    ///
162    /// # Returns
163    ///
164    /// `Some(error)` for [`Self::Failed`], or `None` for task panics because a
165    /// panic payload is not an error source.
166    fn source(&self) -> Option<&(dyn Error + 'static)> {
167        match self {
168            Self::Failed(error) => Some(error),
169            Self::Panicked { .. } => None,
170        }
171    }
172}
173
174/// Converts a panic payload into a task panic error.
175///
176/// # Parameters
177///
178/// * `payload` - Panic payload captured by `catch_unwind`.
179///
180/// # Returns
181///
182/// A panicked task error containing a string message when the payload carries
183/// one.
184pub(crate) fn panic_payload_to_error<E>(payload: &(dyn Any + Send)) -> BatchTaskError<E> {
185    BatchTaskError::from_panic_payload(payload)
186}
187
188/// Extracts a readable panic message from a panic payload.
189///
190/// # Parameters
191///
192/// * `payload` - Panic payload captured by `catch_unwind`.
193///
194/// # Returns
195///
196/// A cloned panic message when `payload` is `&'static str` or `String`.
197fn panic_payload_message(payload: &(dyn Any + Send)) -> Option<String> {
198    if let Some(message) = payload.downcast_ref::<&'static str>() {
199        Some((*message).to_owned())
200    } else {
201        payload.downcast_ref::<String>().cloned()
202    }
203}