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}