snarkvm_utilities/
errors.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use std::{borrow::Borrow, fmt::Display};
17
18/// Generates an `io::Error` from the given string.
19#[inline]
20pub fn io_error<S: ToString>(err: S) -> std::io::Error {
21    std::io::Error::other(err.to_string())
22}
23
24/// Generates an `io::Error` from the given `anyhow::Error`.
25///
26/// This will flatten the existing error chain so that it fits in a single-line string.
27#[inline]
28pub fn into_io_error<E: Into<anyhow::Error>>(err: E) -> std::io::Error {
29    let err: anyhow::Error = err.into();
30    std::io::Error::other(flatten_anyhow_error(&err))
31}
32
33/// Helper function for `log_error` and `log_warning`.
34#[inline]
35fn flatten_anyhow_error<E: Borrow<anyhow::Error>>(error: E) -> String {
36    let error = error.borrow();
37    let mut output = error.to_string();
38    for next in error.chain().skip(1) {
39        output = format!("{output} — {next}");
40    }
41    output
42}
43
44/// Logs `anyhow::Error`'s its error chain using the `ERROR` log level.
45///
46/// This follows the existing convention in the codebase that joins errors using em dashes.
47/// For example, an error "Invalid transaction" with a cause "Proof failed" would be logged
48/// as "Invalid transaction — Proof failed".
49#[inline]
50#[track_caller]
51pub fn log_error<E: Borrow<anyhow::Error>>(error: E) {
52    tracing::error!("{}", flatten_anyhow_error(error));
53}
54
55/// Logs `anyhow::Error`'s its error chain using the `WARN` log level.
56///
57/// This follows the existing convention in the codebase that joins errors using em dashes.
58/// For example, an error "Invalid transaction" with a cause "Proof failed" would be logged
59/// as "Invalid transaction — Proof failed".
60#[inline]
61#[track_caller]
62pub fn log_warning<E: Borrow<anyhow::Error>>(error: E) {
63    tracing::warn!("{}", flatten_anyhow_error(error));
64}
65
66/// Displays an `anyhow::Error`'s main error and its error chain to stderr.
67///
68/// This can be used to show a "pretty" error to the end user.
69#[track_caller]
70#[inline]
71pub fn display_error(error: &anyhow::Error) {
72    eprintln!("⚠️ {error}");
73    error.chain().skip(1).for_each(|cause| eprintln!("     ↳ {cause}"));
74}
75
76/// Ensures that two values are equal, otherwise bails with a formatted error message.
77///
78/// # Arguments
79/// * `actual` - The actual value
80/// * `expected` - The expected value  
81/// * `message` - A description of what was being checked
82#[macro_export]
83macro_rules! ensure_equals {
84    ($actual:expr, $expected:expr, $message:expr) => {
85        if $actual != $expected {
86            anyhow::bail!("{}: Was {} but expected {}.", $message, $actual, $expected);
87        }
88    };
89}
90
91/// A trait that allows printing the entire error chain of an Error (it is implemented for [`anyhow::Error`]) along with a custom context message.
92///
93/// This reduces the need for custom error printing code and ensures consistency across log messages.
94///
95/// # Example
96/// The following code will log `user-facing message - low level error` as an error.
97///
98/// ```rust
99/// use anyhow::anyhow;
100/// use snarkvm_utilities::LoggableError;
101///
102/// let my_error = anyhow!("low level problem");
103/// my_error.log_error("user-facing message");
104/// ```
105pub trait LoggableError {
106    /// Log the error with the given context and log level `ERROR`.
107    fn log_error<S: Send + Sync + Display + 'static>(self, context: S);
108    /// Log the error with the given context and log level `WARNING`.
109    fn log_warning<S: Send + Sync + Display + 'static>(self, context: S);
110    /// Log the error with the given context and log level `DEBUG`.
111    fn log_debug<S: Send + Sync + Display + 'static>(self, context: S);
112}
113
114impl<E: Into<anyhow::Error>> LoggableError for E {
115    /// Log the error with the given context and log level `ERROR`.
116    #[track_caller]
117    #[inline]
118    fn log_error<S: Send + Sync + Display + 'static>(self, context: S) {
119        let err: anyhow::Error = self.into();
120        log_error(err.context(context));
121    }
122
123    /// Log the error with the given context and log level `WARNING`.
124    #[track_caller]
125    #[inline]
126    fn log_warning<S: Send + Sync + Display + 'static>(self, context: S) {
127        let err: anyhow::Error = self.into();
128        log_warning(err.context(context));
129    }
130
131    /// Log the error with the given context and log level `DEBUG`.
132    #[track_caller]
133    #[inline]
134    fn log_debug<S: Send + Sync + Display + 'static>(self, context: S) {
135        let err: anyhow::Error = self.into();
136        log_warning(err.context(context));
137    }
138}
139
140/// A trait to provide a nicer way to unwarp `anyhow::Result`.
141pub trait PrettyUnwrap {
142    type Inner;
143
144    /// Behaves like [`std::result::Result::unwrap`] but will print the entire anyhow chain to stderr.
145    fn pretty_unwrap(self) -> Self::Inner;
146
147    /// Behaves like [`std::result::Result::expect`] but will print the entire anyhow chain to stderr.
148    fn pretty_expect<S: ToString>(self, context: S) -> Self::Inner;
149}
150
151/// Helper for `PrettyUnwrap`, which creates a panic with the `anyhow::Error` nicely formatted and also logs the panic.
152#[track_caller]
153#[inline]
154fn pretty_panic(error: &anyhow::Error) -> ! {
155    let mut string = format!("⚠️ {error}");
156    error.chain().skip(1).for_each(|cause| string.push_str(&format!("\n     ↳ {cause}")));
157    let caller = std::panic::Location::caller();
158
159    tracing::error!("[{}:{}] {string}", caller.file(), caller.line());
160    panic!("{string}");
161}
162
163/// Implement the trait for `anyhow::Result`.
164impl<T> PrettyUnwrap for anyhow::Result<T> {
165    type Inner = T;
166
167    #[track_caller]
168    fn pretty_unwrap(self) -> Self::Inner {
169        match self {
170            Ok(result) => result,
171            Err(error) => {
172                pretty_panic(&error);
173            }
174        }
175    }
176
177    #[track_caller]
178    fn pretty_expect<S: ToString>(self, context: S) -> Self::Inner {
179        match self {
180            Ok(result) => result,
181            Err(error) => {
182                pretty_panic(&error.context(context.to_string()));
183            }
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::{PrettyUnwrap, flatten_anyhow_error, pretty_panic};
191
192    use anyhow::{Context, Result, anyhow, bail};
193
194    const ERRORS: [&str; 3] = ["Third error", "Second error", "First error"];
195
196    #[test]
197    fn flatten_error() {
198        let expected = format!("{} — {} — {}", ERRORS[0], ERRORS[1], ERRORS[2]);
199
200        let my_error = anyhow!(ERRORS[2]).context(ERRORS[1]).context(ERRORS[0]);
201        let result = flatten_anyhow_error(&my_error);
202
203        assert_eq!(result, expected);
204    }
205
206    #[test]
207    fn chained_error_panic_format() {
208        let expected = format!("⚠️ {}\n     ↳ {}\n     ↳ {}", ERRORS[0], ERRORS[1], ERRORS[2]);
209
210        let result = std::panic::catch_unwind(|| {
211            let my_error = anyhow!(ERRORS[2]).context(ERRORS[1]).context(ERRORS[0]);
212            pretty_panic(&my_error);
213        })
214        .unwrap_err();
215
216        assert_eq!(*result.downcast::<String>().expect("Error was not a string"), expected);
217    }
218
219    #[test]
220    fn chained_pretty_unwrap_format() {
221        let expected = format!("⚠️ {}\n     ↳ {}\n     ↳ {}", ERRORS[0], ERRORS[1], ERRORS[2]);
222
223        // Also test `pretty_unwrap` and chaining errors across functions.
224        let result = std::panic::catch_unwind(|| {
225            fn level2() -> Result<()> {
226                bail!(ERRORS[2]);
227            }
228
229            fn level1() -> Result<()> {
230                level2().with_context(|| ERRORS[1])?;
231                Ok(())
232            }
233
234            fn level0() -> Result<()> {
235                level1().with_context(|| ERRORS[0])?;
236                Ok(())
237            }
238
239            level0().pretty_unwrap();
240        })
241        .unwrap_err();
242
243        assert_eq!(*result.downcast::<String>().expect("Error was not a string"), expected);
244    }
245
246    /// Ensure catch_unwind does not break `try_vm_runtime`.
247    #[test]
248    fn test_nested_with_try_vm_runtime() {
249        use crate::try_vm_runtime;
250
251        let result = std::panic::catch_unwind(|| {
252            // try_vm_runtime uses catch_unwind internally
253            let vm_result = try_vm_runtime!(|| {
254                panic!("VM operation failed!");
255            });
256
257            assert!(vm_result.is_err(), "try_vm_runtime should catch VM panic");
258
259            // We can handle the VM error gracefully
260            "handled_vm_error"
261        });
262
263        assert!(result.is_ok(), "Should handle VM error gracefully");
264        assert_eq!(result.unwrap(), "handled_vm_error");
265    }
266}