rust_try_catch/
lib.rs

1#![deny(missing_docs)]
2
3#![doc = include_str!("../README.md")]
4
5use std::any::Any;
6use std::backtrace::Backtrace;
7use std::panic::AssertUnwindSafe;
8
9#[cfg(not(panic = "unwind"))]
10compile_error!("Try catch only works when panic = \"unwind\"");
11
12/// A flexible `try_catch` macro that provides structured error and panic handling
13/// with an optional `finally` block for cleanup.
14///
15/// # Description
16/// The `try_catch!` macro allows you to define a `try` block for executing code, followed by multiple `catch`
17/// blocks for handling exceptions or panics. Additionally, a `finally` block can be specified for cleanup actions.
18///
19/// # Syntax
20/// ```text
21/// try_catch! {
22///     try {
23///         <try_block>
24///     } catch (<exception_pattern> => <exception_type>) {
25///         <catch_block>
26///     } catch exception (<catch_all_pattern>) {
27///         <catch_all_block>
28///     } catch panic (<panic_pattern>) {
29///         <catch_panic_block>
30///     } finally {
31///         <finally_block>
32///     }
33/// }
34/// ```
35///
36/// - **try block**: The primary code to execute.
37/// - **catch blocks**: Handle exceptions matching specific types.
38/// - **catch exception block** (optional): A generic handler for exceptions not caught by specific `catch` blocks.
39/// - **catch panic block** (optional): Handle non-exception panics.
40/// - **finally block** (optional): Executes unconditionally after the `try` block and any `catch` blocks.
41///
42/// ## Notes
43///  - at least 2 blocks have to be defined you cannot have a bare try {} expression
44///  - the try and catch blocks should both return the same type
45///  - the finally block should return () aka "unit"
46///
47/// # Features
48///
49/// - Matches exceptions based on type.
50/// - Catches generic exceptions with `catch exception`.
51/// - Handles panics using `catch panic`.
52/// - Ensures cleanup via an optional `finally` block.
53///
54/// # Usage
55///
56/// ## Handling specific exceptions
57/// ```
58/// #[derive(Debug)]
59/// struct MyErrorType;
60///
61/// fn some_function() -> Result<i32, MyErrorType> {
62///     Err(MyErrorType)
63/// }
64///
65/// let result = rust_try_catch::try_catch! {
66///     try {
67///         rust_try_catch::tri!(some_function())
68///     } catch (e => MyErrorType) {
69///         println!("Caught MyErrorType: {e:?}");
70///         -1
71///     }
72/// };
73/// assert_eq!(result, -1);
74/// ```
75///
76/// ## Catching all exceptions
77/// ```
78/// # fn another_function() {
79/// #     rust_try_catch::throw("Haha I failed");
80/// # }
81///
82/// let result = rust_try_catch::try_catch! {
83///     try {
84///         // Code that might throw.
85///         another_function();
86///         0
87///     } catch exception (e) {
88///         println!("Caught an exception: {:?}", e);
89///         -2
90///     }
91/// };
92/// assert_eq!(result, -2);
93/// ```
94///
95/// ## Handling panics
96/// ```
97/// let result = rust_try_catch::try_catch! {
98///     try {
99///         // Code that might panic.
100///         panic!("Unexpected error");
101///         0
102///     } catch panic (e) {
103///         println!("Caught a panic: {:?}", e);
104///         -101
105///     }
106/// };
107/// assert_eq!(result, -101);
108/// ```
109///
110/// ## Using a finally block
111/// ```
112/// let mut cleanup = false;
113/// let result = rust_try_catch::try_catch! {
114///     try {
115///         // Code execution.
116///         42
117///     } finally {
118///         cleanup = true;
119///     }
120/// };
121/// assert_eq!(result, 42);
122/// assert!(cleanup);
123/// ```
124///
125/// ## Combining handlers
126/// ```
127/// # #[derive(Debug)]
128/// # struct SpecificError;
129/// # let risky_operation = || rust_try_catch::throw(SpecificError);
130///
131/// let result = rust_try_catch::try_catch! {
132///     try {
133///         // Code execution.
134///         risky_operation();
135///         0
136///     } catch (e => SpecificError) {
137///         println!("Caught SpecificError: {e:?}");
138///         -1
139///     } catch exception (e) {
140///         println!("Caught general exception: {e:?}");
141///         -2
142///     } catch panic (e) {
143///         println!("Caught a panic: {e:?}");
144///         -3
145///     } finally {
146///         println!("Cleanup actions here.");
147///     }
148/// };
149/// ```
150///
151/// # Notes
152///
153/// - The `catch panic` block is only invoked for panics unrelated to exceptions handled by the macro.
154/// - The `finally` block runs regardless of whether an exception or panic occurred.
155/// - Unhandled exceptions or panics will propagate out of the macro.
156///
157/// # Examples
158///
159/// ## No exception or panic (doesn't compile)
160/// ```compile_fail
161/// let result = rust_try_catch::try_catch! {
162///     try {
163///         100
164///     }
165/// };
166/// assert_eq!(result, 100);
167/// ```
168///
169/// ## Exception without panic
170/// ```
171/// # use rust_try_catch::throw;
172///
173/// let result = rust_try_catch::try_catch! {
174///     try {
175///         throw("An error occurred");
176///     } catch (e => &'static str) {
177///         println!("Handled error: {}", e);
178///         0
179///     }
180/// };
181/// assert_eq!(result, 0);
182/// ```
183///
184/// ## Panic recovery
185/// ```
186/// let result = rust_try_catch::try_catch! {
187///     try {
188///         panic!("Something went wrong!");
189///     } catch panic (e) {
190///         println!("Recovered from panic: {:?}", e);
191///         1
192///     }
193/// };
194/// assert_eq!(result, 1);
195/// ```
196#[macro_export]
197macro_rules! try_catch {
198    {
199        try {
200            $($try_body: tt)*
201        } $(catch ($exception_name: pat => $exception_ty:ty) {
202            $($catch_body: tt)*
203        })* $(catch exception ($catch_all_exception_name: pat) {
204            $($catch_all_exception_body: tt)*
205        })? $(catch panic ($catch_panic_exception_name: pat) {
206            $($catch_panic_exception_body: tt)*
207        })? $(finally {
208            $($finally_body: tt)*
209        })?
210    } => {{
211        const {
212            let count = $crate::__count_blocks!(
213                {$($try_body)*}
214                $({$exception_name})*
215                $({$catch_all_exception_name})?
216                $({$catch_panic_exception_name})?
217                $({$($finally_body)*})?
218            );
219
220            if count < 2 {
221                ::core::panic!("Using try {{ /*code*/ }} is equivalent to a no-op")
222            }
223        }
224
225        struct FinallyDo<F: ::core::ops::FnOnce() -> ()>(::core::mem::ManuallyDrop<F>);
226        impl<F: ::core::ops::FnOnce()> Drop for FinallyDo<F> {
227            fn drop(&mut self) {
228                (unsafe { ::core::mem::ManuallyDrop::take(&mut self.0) })()
229            }
230        }
231
232        $(let _finally_guard = FinallyDo(::core::mem::ManuallyDrop::new(|| {
233            $($finally_body)*
234        }));)?
235
236        let fun = ::std::panic::AssertUnwindSafe(|| { $($try_body)* });
237        let val = match ::std::panic::catch_unwind(fun) {
238            Ok(res) => res,
239            Err(panic_payload) => 'ret_from_err: {
240                let mut exception = match panic_payload.downcast::<$crate::Thrown>() {
241                    Ok(box_thrown) => box_thrown,
242                    Err(normal_panic) => {
243                        $({
244                            let $catch_panic_exception_name = normal_panic;
245                            break 'ret_from_err ({$($catch_panic_exception_body)*})
246                        })?
247                        #[allow(unreachable_code)]
248                        ::std::panic::resume_unwind(normal_panic)
249                    }
250                };
251
252                $(
253                    match exception.source.downcast::<$exception_ty>() {
254                        Ok(box_error) => {
255                            let $exception_name: $exception_ty = *box_error;
256
257                            break 'ret_from_err ({
258                               $($catch_body)*
259                            })
260                        }
261                        Err(other_error) => exception.source = other_error,
262                    }
263                )*
264
265                $({
266                    let $catch_all_exception_name = exception.source;
267                    break 'ret_from_err ({$($catch_all_exception_body)*})
268                })?
269
270                #[allow(unreachable_code)]
271                ::std::panic::resume_unwind(exception)
272            }
273        };
274
275        val
276    }};
277}
278
279/// Unwraps a result or propagates its error as an exception.
280///
281/// tri! matches the given Result.
282/// In case of the Ok variant, the expression has the value of the wrapped value.
283/// In case of the Err variant, it retrieves the inner error, and calls throw on it.
284#[macro_export]
285macro_rules! tri {
286    ($expr: expr) => {
287        match ($expr) {
288            ::core::result::Result::Ok(val) => val,
289            ::core::result::Result::Err(err) => $crate::throw(err),
290        }
291    };
292}
293
294#[doc(hidden)]
295pub struct Thrown {
296    pub source: Box<dyn Any + Send>,
297    pub type_name: &'static str,
298    pub backtrace: Backtrace
299}
300
301/// Calling throw always results in a panic
302/// 
303/// for proper usage users must ensure that there is a function annotated with `rust_try_catch::throw_guard`
304/// up in the call chain
305pub fn throw<T: Any + Send + 'static>(x: T) -> ! {
306    std::panic::resume_unwind(Box::new(Thrown {
307        source: Box::new(x),
308        type_name: std::any::type_name::<T>(),
309        backtrace: Backtrace::force_capture()
310    }))
311}
312
313/// # Description
314/// wraps a function or closure, to prevent a thrown exception from propagating beyond them
315/// and turns unhandled exceptions to a panic
316///
317/// # Note
318/// thrown exceptions do not trigger the panic hook so if this isn't in the call chain before some code
319/// throws, the process might exit abruptly due to a panic with an unspecified load
320pub use rust_try_catch_macros::{throw_guard, closure_throw_guard};
321
322
323#[doc(hidden)]
324#[track_caller]
325pub fn __throw_driver<T>(main: impl FnOnce() -> T) -> T {
326    // help reduce size of throw_driver
327    #[inline(never)]
328    fn inner(f: &mut dyn FnMut()) {
329        if let Err(panic) = std::panic::catch_unwind(AssertUnwindSafe(f)) {
330            if let Some(Thrown { type_name, backtrace, .. }) = panic.downcast_ref() {
331                panic!("unhandled exception {type_name} at {backtrace}");
332            }
333
334            std::panic::resume_unwind(panic)
335        }
336    }
337    
338    let mut main = Some(main);
339    let mut output = None;
340    inner(&mut || {
341        // Safety: inner runs `f` at most once
342        unsafe {
343            let main_fn = main.take().unwrap_unchecked();
344            // help skip destructor call
345            std::hint::assert_unchecked(output.is_none());
346            output = Some(main_fn())
347        }
348    });
349
350    // Safety: if inner returns that means the closure ran to completion
351    unsafe { output.unwrap_unchecked() }
352}
353
354#[doc(hidden)]
355#[macro_export]
356macro_rules! __count_blocks {
357    () => { 0 };
358    ({$($tt:tt)*} $($rest:tt)*) => {
359        1 + $crate::__count_blocks!($($rest)*)
360    }
361}