Skip to main content

charon_error/
panic_hook.rs

1/// Set up a custom panic hook with issue submission and known-error matching.
2///
3/// Replaces the default panic handler with one that:
4/// - Checks if the panic matches a known error (via `$check_if_known_error`)
5/// - In release builds: shows a user-friendly message with a report link
6/// - In debug builds: shows the full error report with a clickable issue URL
7///
8/// Only active when `RUST_BACKTRACE` is not set (falls back to default otherwise).
9///
10/// # Arguments
11///
12/// - `$submit_error_report` — A type implementing [`SubmitErrorReport`](crate::SubmitErrorReport) (e.g. `GitLabErrorReport`)
13/// - `$check_if_known_error` — A function `fn(&str) -> Option<String>` that returns
14///   a message if the panic matches a known issue, or `None` to generate a report
15///
16/// # Example
17///
18/// ```rust,no_run
19/// use charon_error::prelude::*;
20/// use charon_error::prelude::gitlab_er::*;
21///
22/// fn check_if_common_errors(msg: &str) -> Option<String> {
23///     if msg.contains("out of memory") {
24///         Some("Try closing other applications".to_owned())
25///     } else {
26///         None
27///     }
28/// }
29///
30/// fn main() {
31///     # return; // Skip actual execution in doc tests
32///     setup_panic!(GitLabErrorReport, check_if_common_errors);
33/// }
34/// ```
35#[macro_export]
36macro_rules! setup_panic {
37    ($submit_error_report:ty, $check_if_known_error:ident) => {
38        // This code is inspired by the `human-panic` crate.
39        // Do not use custom error messages if `RUST_BACKTRACE` is set
40        if ::std::env::var("RUST_BACKTRACE").is_err() {
41            #[allow(unused_imports)]
42            use charon_error::prelude::panic_hook::*;
43            #[allow(unused_imports)]
44            use std::panic::{self, PanicHookInfo};
45
46            panic::set_hook(Box::new(move |info: &PanicHookInfo| {
47                let payload = info.payload();
48                let panic_message = if let Some(s) = payload.downcast_ref::<&str>() {
49                    s.to_string()
50                } else if let Some(s) = payload.downcast_ref::<String>() {
51                    s.clone()
52                } else if let Some(s) = payload.downcast_ref::<anyhow::Error>() {
53                    s.to_string()
54                } else if let Some(s) = payload.downcast_ref::<ErrorReport>() {
55                    s.get_last_error_title()
56                } else {
57                    String::new()
58                };
59                if let Some(message) = $check_if_known_error(&panic_message) {
60                    // Known Issue
61                    eprintln!(
62                        "{}: The application encountered an error it could not recover from.\n\
63                        This is a known issue: {}\n\
64                        Panic message: {}\n",
65                        "PANIC".bright_red(),
66                        message,
67                        panic_message,
68                    );
69                } else {
70                    // Unknown Issue
71                    let panic_location: Option<SourceLocation> = match info.location() {
72                        Some(location) => Some(SourceLocation::from_panic_location(location)),
73                        None => None,
74                    };
75
76                    eprintln!(
77                        "{}: A panic occurred during execution.\n\
78                        * Message: `{}`\n\
79                        * Path: `{}`",
80                        "PANIC".bright_red(),
81                        panic_message.bright_cyan().bold(),
82                        panic_location.map(|s| s.to_string()).unwrap_or(String::from("Unknown location")),
83                    );
84
85                    let error_report: &ErrorReport = if let Some(s) = payload.downcast_ref::<&str>() {
86                        &ErrorReport::from_error(StringError::new(s.to_string()))
87                    } else if let Some(s) = payload.downcast_ref::<String>() {
88                        &ErrorReport::from_error(StringError::new(s.to_string()))
89                    } else if let Some(s) = payload.downcast_ref::<anyhow::Error>() {
90                        &ErrorReport::from_anyhow_error_ref(s)
91                    } else if let Some(s) = payload.downcast_ref::<ErrorReport>() {
92                        s
93                    } else {
94                        &ErrorReport::from_error(StringError::new(panic_message))
95                    };
96
97                    // --------- Release Build ---------
98                    // Only use custom panic when in release mode.
99                    #[cfg(not(debug_assertions))]
100                    {
101                        let submit_report =
102                            <$submit_error_report as SubmitErrorReport>::new(error_report);
103                        eprintln!(
104                            "{}: The application encountered an error it could not recover from.\n\
105                            If you report this we might be able to fix this in the future.\n\
106                            {title}\n\
107                            {message}",
108                            "PANIC".bright_red(),
109                            title = submit_report.get_title(), // issue.create_message()
110                            message = submit_report.create_message().unwrap_or("Something went wrong while generating error report.".to_owned()),
111                        );
112                    }
113
114                    // --------- Debug Build ---------
115                    // Print different message on Debug builds.
116                    #[cfg(debug_assertions)]
117                    {
118                        let submit_report =
119                            <$submit_error_report as SubmitErrorReport>::new(error_report);
120                        let message = submit_report.error_report.stringify(ErrorFmtSettings::default());
121                        // message = submit_report.create_bug_report().unwrap_or("Something went wrong while generating error report.".to_owned()),
122                        // 2083 max length: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#length-limits
123                        let url = submit_report.create_submit_url_limited(2083).map(|s| s.to_string());
124
125                        match (message, url) {
126                            (Err(m_err), Err(u_err)) => {
127                                eprintln!(
128                                    "PANIC: The application encountered an error it could not recover from.\n\
129                                    This error occurred while handling the Panic of an other error.\n\
130                                    Panic message while creating panic message: {m_err}\n\
131                                    Panic message while creating panic report URL: {u_err}\n"
132                                );
133                            }
134                            (Err(m_err), Ok(_)) => {
135                                eprintln!(
136                                    "PANIC: The application encountered an error it could not recover from.\n\
137                                    This error occurred while handling the Panic of an other error.\n\
138                                    Panic message while creating panic message: {m_err}"
139                                );
140                            }
141                            (Ok(_), Err(u_err)) => {
142                                eprintln!(
143                                    "PANIC: The application encountered an error it could not recover from.\n\
144                                    This error occurred while handling the Panic of an other error.\n\
145                                    Panic message while creating panic report URL: {u_err}"
146                                );
147                            }
148                            (Ok(message), Ok(url)) => {
149                                eprintln!(
150                                    "{}: The application encountered an error it could not recover from.\n\
151                                    {message}\n\
152                                    ┌──────────────────────────┐\n\
153                                    │ \x1b]8;;{url}\x1b\\\u{f296} Open/Submit Bug Report\x1b]8;;\x1b\\ │\n\
154                                    └──────────────────────────┘",
155                                    "PANIC".bright_red(),
156                                );
157                            }
158                        }
159                    }
160                }
161            }));
162        }
163    };
164}
165
166/// Set up a simple panic hook that prints a human-readable error message.
167///
168/// A lighter alternative to [`setup_panic!`] that does not generate issue
169/// submission URLs or check for known errors. Just prints the panic message
170/// and source location.
171#[macro_export]
172macro_rules! setup_panic_simple {
173    () => {{
174        #[allow(unused_imports)]
175        use charon_error::prelude::panic_hook::*;
176        #[allow(unused_imports)]
177        use std::panic::{self, PanicHookInfo};
178
179        panic::set_hook(Box::new(move |info: &PanicHookInfo| {
180            let payload = info.payload();
181            let panic_message = if let Some(s) = payload.downcast_ref::<&str>() {
182                s.to_string()
183            } else if let Some(s) = payload.downcast_ref::<String>() {
184                s.clone()
185            } else {
186                String::new()
187            };
188            let panic_location: Option<SourceLocation> = match info.location() {
189                Some(location) => Some(SourceLocation::from_panic_location(location)),
190                None => None,
191            };
192            eprintln!(
193                "PANIC: The application encountered an error it could not recover from.\n\
194                Panic message: {}\n\
195                Location: {}",
196                panic_message,
197                panic_location
198                    .map(|s| s.to_string())
199                    .unwrap_or(String::from("Unknown location")),
200            );
201        }));
202    }};
203}