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::<AnyhowError>() {
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.as_ref()
83                            .map(|s| s.display_location(None))
84                            .unwrap_or(String::from("Unknown location")),
85                    );
86
87                    let error_report: &ErrorReport = if let Some(s) = payload.downcast_ref::<&str>() {
88                        &ErrorReport::from_error(StringError::new(s.to_string()))
89                    } else if let Some(s) = payload.downcast_ref::<String>() {
90                        &ErrorReport::from_error(StringError::new(s.to_string()))
91                    } else if let Some(s) = payload.downcast_ref::<AnyhowError>() {
92                        &ErrorReport::from_anyhow_error_ref(s)
93                    } else if let Some(s) = payload.downcast_ref::<ErrorReport>() {
94                        s
95                    } else {
96                        &ErrorReport::from_error(StringError::new(panic_message))
97                    };
98
99                    // --------- Release Build ---------
100                    // Only use custom panic when in release mode.
101                    #[cfg(not(debug_assertions))]
102                    {
103                        let submit_report =
104                            <$submit_error_report as SubmitErrorReport>::new(error_report);
105                        eprintln!(
106                            "{}: The application encountered an error it could not recover from.\n\
107                            If you report this we might be able to fix this in the future.\n\
108                            {title}\n\
109                            {message}",
110                            "PANIC".bright_red(),
111                            title = submit_report.get_title(), // issue.create_message()
112                            message = submit_report.create_message().unwrap_or("Something went wrong while generating error report.".to_owned()),
113                        );
114                        // Log error
115                        let error_report_id = submit_report.get_error_report().get_unique_id();
116                        let error_title = submit_report.get_title();
117                        let panic_location_string = panic_location.as_ref()
118                            .map(|s| s.display_location(Some(LinkDebugIde::NoLink)))
119                            .unwrap_or(String::from("Unknown location"));
120                        tracing::error!(
121                            error_type = "PANIC",
122                            error_report_id = error_report_id,
123                            error_title = error_title,
124                            panic_location = panic_location_string,
125                            "The application encountered an error it could not recover from: {error_title}"
126                        );
127                    }
128
129                    // --------- Debug Build ---------
130                    // Print different message on Debug builds.
131                    #[cfg(debug_assertions)]
132                    {
133                        let submit_report =
134                            <$submit_error_report as SubmitErrorReport>::new(error_report);
135                        let message = submit_report.error_report.stringify(ErrorFmtSettings::default());
136                        // message = submit_report.create_bug_report().unwrap_or("Something went wrong while generating error report.".to_owned()),
137                        // 2083 max length: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#length-limits
138                        let url = submit_report.create_submit_url_limited(2083).map(|s| s.to_string());
139
140                        match (message, url) {
141                            (Err(m_err), 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 message: {m_err}\n\
146                                    Panic message while creating panic report URL: {u_err}\n"
147                                );
148                            }
149                            (Err(m_err), Ok(_)) => {
150                                eprintln!(
151                                    "PANIC: The application encountered an error it could not recover from.\n\
152                                    This error occurred while handling the Panic of an other error.\n\
153                                    Panic message while creating panic message: {m_err}"
154                                );
155                            }
156                            (Ok(_), Err(u_err)) => {
157                                eprintln!(
158                                    "PANIC: The application encountered an error it could not recover from.\n\
159                                    This error occurred while handling the Panic of an other error.\n\
160                                    Panic message while creating panic report URL: {u_err}"
161                                );
162                            }
163                            (Ok(message), Ok(url)) => {
164                                // Disable links too if ANSI is disabled.
165                                let mut report_link = "No report link available.".to_owned();
166                                if !url.starts_with("data:text/plain,") {
167                                    report_link = format!("\
168                                    ┌──────────────────────────┐\n\
169                                    │ \x1b]8;;{url}\x1b\\\u{f296} Open/Submit Bug Report\x1b]8;;\x1b\\ │\n\
170                                    └──────────────────────────┘");
171                                }
172                                if !url.starts_with("data:text/plain,") && std::env::var("NO_COLOR").is_ok() {
173                                    report_link = format!("Report Link: {}", url);
174                                }
175
176                                eprintln!(
177                                    "{}: The application encountered an error it could not recover from.\n\
178                                    {message}\n\
179                                    {report_link}",
180                                    "PANIC".bright_red(),
181                                );
182                            }
183                        }
184                        // Log error
185                        let error_report_id = submit_report.get_error_report().get_unique_id();
186                        let error_title = submit_report.get_title();
187                        let panic_location_string = panic_location.as_ref()
188                            .map(|s| s.display_location(Some(LinkDebugIde::NoLink)))
189                            .unwrap_or(String::from("Unknown location"));
190                        tracing::error!(
191                            error_type = "PANIC",
192                            error_report_id = error_report_id,
193                            error_title = error_title,
194                            panic_location = panic_location_string,
195                            "The application encountered an error it could not recover from: {error_title}"
196                        );
197                    }
198                }
199            }));
200        }
201    };
202}
203
204/// Set up a simple panic hook that prints a human-readable error message.
205///
206/// A lighter alternative to [`setup_panic!`] that does not generate issue
207/// submission URLs or check for known errors. Just prints the panic message
208/// and source location.
209#[macro_export]
210macro_rules! setup_panic_simple {
211    () => {{
212        #[allow(unused_imports)]
213        use charon_error::prelude::panic_hook::*;
214        #[allow(unused_imports)]
215        use std::panic::{self, PanicHookInfo};
216
217        panic::set_hook(Box::new(move |info: &PanicHookInfo| {
218            let payload = info.payload();
219            let panic_message = if let Some(s) = payload.downcast_ref::<&str>() {
220                s.to_string()
221            } else if let Some(s) = payload.downcast_ref::<String>() {
222                s.clone()
223            } else if let Some(s) = payload.downcast_ref::<AnyhowError>() {
224                s.to_string()
225            } else if let Some(s) = payload.downcast_ref::<ErrorReport>() {
226                s.get_last_error_title()
227            } else {
228                String::new()
229            };
230            let panic_location: Option<SourceLocation> = match info.location() {
231                Some(location) => Some(SourceLocation::from_panic_location(location)),
232                None => None,
233            };
234            eprintln!(
235                "PANIC: The application encountered an error it could not recover from.\n\
236                * Message: '{panic_message}'\n\
237                * Path: '{}'",
238                panic_location
239                    .as_ref()
240                    .map(|s| s.display_location(None))
241                    .unwrap_or(String::from("Unknown location"))
242            );
243            // Log error
244            let panic_title = panic_message;
245            let panic_location_string = panic_location
246                .as_ref()
247                .map(|s| s.display_location(Some(LinkDebugIde::NoLink)))
248                .unwrap_or(String::from("Unknown location"));
249            tracing::error!(
250                error_type = "PANIC",
251                panic_location = panic_location_string,
252                panic_title = panic_title,
253                "The application encountered an error it could not recover from: {panic_title}"
254            );
255        }));
256    }};
257}