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
16/// Generates an `io::Error` from the given string.
17pub fn io_error<S: ToString>(err: S) -> std::io::Error {
18    std::io::Error::other(err.to_string())
19}
20
21/// Generates an `io::Error` from the given `anyhow::Error`.
22///
23/// This will flatten the existing error chain so that it fits in a single-line string.
24pub fn into_io_error<E: Into<anyhow::Error>>(err: E) -> std::io::Error {
25    let err: anyhow::Error = err.into();
26    std::io::Error::other(flatten_anyhow_error(&err))
27}
28
29/// Helper function for `log_error` and `log_warning`.
30#[inline]
31fn flatten_anyhow_error(error: &anyhow::Error) -> String {
32    let mut output = error.to_string();
33    for next in error.chain().skip(1) {
34        output = format!("{output} — {next}");
35    }
36    output
37}
38
39/// Logs `anyhow::Error`'s its error chain using the `ERROR` log level.
40///
41/// This follows the existing convention in the codebase that joins errors using em dashes.
42/// For example, an error "Invalid transaction" with a cause "Proof failed" would be logged
43/// as "Invalid transaction — Proof failed".
44pub fn log_error(error: &anyhow::Error) {
45    tracing::error!("{}", flatten_anyhow_error(error));
46}
47
48/// Logs `anyhow::Error`'s its error chain using the `WARN` log level.
49///
50/// This follows the existing convention in the codebase that joins errors using em dashes.
51/// For example, an error "Invalid transaction" with a cause "Proof failed" would be logged
52/// as "Invalid transaction — Proof failed".
53pub fn log_warning(error: &anyhow::Error) {
54    tracing::warn!("{}", flatten_anyhow_error(error));
55}
56
57/// Displays an `anyhow::Error`'s main error and its error chain to stderr.
58///
59/// This can be used to show a "pretty" error to the end user.
60pub fn display_error(error: &anyhow::Error) {
61    eprintln!("⚠️ {error}");
62    error.chain().skip(1).for_each(|cause| eprintln!("     ↳ {cause}"));
63}
64
65/// Ensures that two values are equal, otherwise bails with a formatted error message.
66///
67/// # Arguments
68/// * `actual` - The actual value
69/// * `expected` - The expected value  
70/// * `message` - A description of what was being checked
71#[macro_export]
72macro_rules! ensure_equals {
73    ($actual:expr, $expected:expr, $message:expr) => {
74        if $actual != $expected {
75            anyhow::bail!("{}: Was {} but expected {}.", $message, $actual, $expected);
76        }
77    };
78}
79
80/// A trait to provide a nicer way to unwarp `anyhow::Result`.
81pub trait PrettyUnwrap {
82    type Inner;
83
84    /// Behaves like [`std::Result::unwrap`] but will print the entire anyhow chain to stderr.
85    fn pretty_unwrap(self) -> Self::Inner;
86}
87
88/// Helper for `PrettyUnwrap`, which creates a panic with the `anyhow::Error` nicely formatted and also logs the panic.
89#[track_caller]
90#[inline]
91fn pretty_panic(error: &anyhow::Error) -> ! {
92    let mut string = format!("⚠️ {error}");
93    error.chain().skip(1).for_each(|cause| string.push_str(&format!("\n     ↳ {cause}")));
94    let caller = std::panic::Location::caller();
95
96    tracing::error!("[{}:{}] {string}", caller.file(), caller.line());
97    panic!("{string}");
98}
99
100/// Implement the trait for `anyhow::Result`.
101impl<T> PrettyUnwrap for anyhow::Result<T> {
102    type Inner = T;
103
104    #[track_caller]
105    fn pretty_unwrap(self) -> Self::Inner {
106        match self {
107            Ok(result) => result,
108            Err(error) => {
109                pretty_panic(&error);
110            }
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::{PrettyUnwrap, flatten_anyhow_error, pretty_panic};
118
119    use anyhow::{Context, Result, anyhow, bail};
120
121    const ERRORS: [&str; 3] = ["Third error", "Second error", "First error"];
122
123    #[test]
124    fn flatten_error() {
125        let expected = format!("{} — {} — {}", ERRORS[0], ERRORS[1], ERRORS[2]);
126
127        let my_error = anyhow!(ERRORS[2]).context(ERRORS[1]).context(ERRORS[0]);
128        let result = flatten_anyhow_error(&my_error);
129
130        assert_eq!(result, expected);
131    }
132
133    #[test]
134    fn chained_error_panic_format() {
135        let expected = format!("⚠️ {}\n     ↳ {}\n     ↳ {}", ERRORS[0], ERRORS[1], ERRORS[2]);
136
137        let result = std::panic::catch_unwind(|| {
138            let my_error = anyhow!(ERRORS[2]).context(ERRORS[1]).context(ERRORS[0]);
139            pretty_panic(&my_error);
140        })
141        .unwrap_err();
142
143        assert_eq!(*result.downcast::<String>().expect("Error was not a string"), expected);
144    }
145
146    #[test]
147    fn chained_pretty_unwrap_format() {
148        let expected = format!("⚠️ {}\n     ↳ {}\n     ↳ {}", ERRORS[0], ERRORS[1], ERRORS[2]);
149
150        // Also test `pretty_unwrap` and chaining errors across functions.
151        let result = std::panic::catch_unwind(|| {
152            fn level2() -> Result<()> {
153                bail!(ERRORS[2]);
154            }
155
156            fn level1() -> Result<()> {
157                level2().with_context(|| ERRORS[1])?;
158                Ok(())
159            }
160
161            fn level0() -> Result<()> {
162                level1().with_context(|| ERRORS[0])?;
163                Ok(())
164            }
165
166            level0().pretty_unwrap();
167        })
168        .unwrap_err();
169
170        assert_eq!(*result.downcast::<String>().expect("Error was not a string"), expected);
171    }
172
173    /// Ensure catch_unwind does not break `try_vm_runtime`.
174    #[test]
175    fn test_nested_with_try_vm_runtime() {
176        use crate::try_vm_runtime;
177
178        let result = std::panic::catch_unwind(|| {
179            // try_vm_runtime uses catch_unwind internally
180            let vm_result = try_vm_runtime!(|| {
181                panic!("VM operation failed!");
182            });
183
184            assert!(vm_result.is_err(), "try_vm_runtime should catch VM panic");
185
186            // We can handle the VM error gracefully
187            "handled_vm_error"
188        });
189
190        assert!(result.is_ok(), "Should handle VM error gracefully");
191        assert_eq!(result.unwrap(), "handled_vm_error");
192    }
193}