Skip to main content

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