Skip to content

Commit 2d4017b

Browse files
authored
Merge pull request #1266 from input-output-hk/ensemble/798-adr-errors
Add errors standards ADR
2 parents a3f0504 + 37b0a1e commit 2d4017b

File tree

2 files changed

+301
-0
lines changed

2 files changed

+301
-0
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ Make sure to follow our [Coding Standards](https://github.com/input-output-hk/mi
8282
It includes guidelines on Rust code style, but also on Git commit messages
8383
and some processes. To propose new standards or changes to the existing standards, file an issue.
8484

85+
Also make sure to follow our Architectural Decision Records or [ADRs](https://mithril.network/doc/adr) that will give you insights on development guidelines and best practices for the project.
86+
8587
### Creating a pull request
8688

8789
Thank you for contributing your changes by opening a pull requests! To get
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
---
2+
slug: 6
3+
title: |
4+
6. Errors implementation Standard
5+
authors:
6+
- name: Mithril Team
7+
tags: [Draft]
8+
date: 2023-09-27
9+
---
10+
11+
## Status
12+
13+
Draft
14+
15+
## Context
16+
17+
Error handling is difficult with Rust:
18+
- Many ways of implementing them with different crates ([`thiserror`](https://crates.io/crates/thiserror), [`anyhow`](https://crates.io/crates/anyhow), ...)
19+
- No exception like handling of errors
20+
- No stack trace or context available by default
21+
- Backtrace uniquely when a panic occurs and if `RUST_BACKTRACE` environment variable is set to `1` or `full`
22+
23+
We think the errors handling should be done in a consistent way in the project.
24+
Thus we have worked on a standardization of their implementation and tried to apply it to the whole repository.
25+
This has enabled us to have a clear vision of the do and don't that we intend to summarize in this ADR.
26+
27+
## Decision
28+
29+
_Therefore_
30+
31+
* We have decided to use `thiserror` and `anyhow` crates to implement the errors:
32+
* [`thiserror`](https://crates.io/crates/thiserror) is used to create module or domain errors that come from our developments and can be easily identified (as they are strongly typed).
33+
* [`anyhow`](https://crates.io/crates/anyhow) is used to add a context to an error triggered by a sub-system. The context is a convenient way to get 'stack trace' like debug information.
34+
35+
Here is a [Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=bf667c443696beb90106f6ae627a57b9) that summarizes the usage of `thiserror`:
36+
37+
```rust
38+
#[allow(unused_imports)]
39+
use anyhow::{anyhow, Context, Result}; // 1.0.71
40+
use thiserror::Error; // 1.0.43
41+
42+
#[derive(Error, Debug)]
43+
#[error("Codec error: {msg}")]
44+
pub struct CodecError {
45+
msg: String,
46+
#[source] // optional if field name is `source`
47+
source: anyhow::Error,
48+
}
49+
50+
#[derive(Error, Debug)]
51+
pub enum DomainError {
52+
#[error("Error with codec: {0:?}")]
53+
CodecWithOnlyDebug(CodecError),
54+
55+
#[error("Error with codec")]
56+
CodecWithSource(#[source] CodecError),
57+
58+
#[error("Error with codec: {0}")]
59+
CodecWithoutAnything(CodecError),
60+
61+
#[error("Anyhow error: {0:?}")]
62+
AnyhowWrapWithOnlyDebug(anyhow::Error),
63+
64+
#[error("Anyhow error")]
65+
AnyhowWrapWithSource(#[source] anyhow::Error),
66+
67+
#[error("Anyhow error: {0}")]
68+
AnyhowWrapWithoutAnything(anyhow::Error),
69+
}
70+
71+
fn anyhow_result() -> Result<()> {
72+
"invalid_number"
73+
.parse::<u64>()
74+
.map(|_| ())
75+
.with_context(|| "Reading database failure")
76+
}
77+
78+
fn thiserror_struct() -> Result<(), CodecError> {
79+
Err(CodecError {
80+
msg: "My message".to_string(),
81+
source: anyhow!("Could not decode config"),
82+
})?;
83+
Ok(())
84+
}
85+
86+
fn print_error(title: &str, error: anyhow::Error) {
87+
println!("{title:-^80}");
88+
println!("{error:?}\n",);
89+
}
90+
91+
fn main() {
92+
println!("1 - Printing errors from enum variant that contains a error struct\n");
93+
// Debug the inner error struct: "normal" debug without the anyhow touch
94+
print_error(
95+
"DomainError::CodecWithOnlyDebug",
96+
anyhow!(DomainError::CodecWithOnlyDebug(
97+
thiserror_struct().unwrap_err()
98+
)),
99+
);
100+
// marking the inner error struct as source: anyhow will be able to make a
101+
// stacktrace out of this error. Nice !
102+
print_error(
103+
"DomainError::CodecWithSource",
104+
anyhow!(DomainError::CodecWithSource(
105+
thiserror_struct().unwrap_err()
106+
)),
107+
);
108+
// without debugging the inner error: only show the error text
109+
print_error(
110+
"DomainError::CodecWithoutAnything",
111+
anyhow!(DomainError::CodecWithoutAnything(
112+
thiserror_struct().unwrap_err()
113+
)),
114+
);
115+
116+
println!("\n2 - Printing errors from enum variant that contains a anyhow error\n");
117+
// using only debug: the first two errors of the stack will be merged
118+
print_error(
119+
"DomainError::AnyhowWrapWithOnlyDebug",
120+
anyhow!(DomainError::AnyhowWrapWithOnlyDebug(
121+
anyhow_result().with_context(|| "context").unwrap_err()
122+
)),
123+
);
124+
// using #[source] attribute: each error of the stack will have a line
125+
print_error(
126+
"DomainError::AnyhowWrapWithSource",
127+
anyhow!(DomainError::AnyhowWrapWithSource(
128+
anyhow_result().with_context(|| "context").unwrap_err()
129+
)),
130+
);
131+
// without debug nor source: only the uppermost error is print
132+
print_error(
133+
"DomainError::AnyhowWrapWithoutAnything",
134+
anyhow!(DomainError::AnyhowWrapWithoutAnything(
135+
anyhow_result().with_context(|| "context").unwrap_err()
136+
)),
137+
);
138+
}
139+
140+
```
141+
142+
Which will output errors this way:
143+
144+
```
145+
1 - Printing errors from enum variant that contains a error struct
146+
147+
------------------------DomainError::CodecWithOnlyDebug-------------------------
148+
Error with codec: CodecError { msg: "My message", source: Could not decode config }
149+
150+
--------------------------DomainError::CodecWithSource--------------------------
151+
Error with codec
152+
153+
Caused by:
154+
0: Codec error: My message
155+
1: Could not decode config
156+
157+
-----------------------DomainError::CodecWithoutAnything------------------------
158+
Error with codec: Codec error: My message
159+
160+
161+
2 - Printing errors from enum variant that contains a anyhow error
162+
163+
----------------------DomainError::AnyhowWrapWithOnlyDebug----------------------
164+
Anyhow error: context
165+
166+
Caused by:
167+
0: Reading database failure
168+
1: invalid digit found in string
169+
170+
-----------------------DomainError::AnyhowWrapWithSource------------------------
171+
Anyhow error
172+
173+
Caused by:
174+
0: context
175+
1: Reading database failure
176+
2: invalid digit found in string
177+
178+
---------------------DomainError::AnyhowWrapWithoutAnything---------------------
179+
Anyhow error: context
180+
```
181+
182+
Here is a [Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=90f962ab001d2ea0321fc5da0d4ec0f1) that summarizes the usage of the `context` feature form `anyhow`:
183+
184+
```rust
185+
#[allow(unused_imports)]
186+
use anyhow::{anyhow, Context, Result}; // 1.0.71
187+
188+
fn read_db() -> Result<()> {
189+
"invalid_number"
190+
.parse::<u64>()
191+
.map(|_| ())
192+
.with_context(|| "Reading database failure")
193+
}
194+
195+
fn do_work() -> Result<()> {
196+
read_db().with_context(|| "Important work failed while reading database")
197+
}
198+
199+
fn do_service_work() -> Result<()> {
200+
do_work().with_context(|| "Service could not do the important work")
201+
}
202+
203+
fn main() {
204+
let error = do_service_work().unwrap_err();
205+
206+
println!("Error string:\n {error}\n\n");
207+
println!("Error debug:\n {error:?}\n\n");
208+
println!("Error pretty:\n {error:#?}\n\n");
209+
}
210+
211+
```
212+
213+
Which will output errors this way:
214+
```
215+
Error string:
216+
Service could not do the important work
217+
218+
219+
Error debug:
220+
Service could not do the important work
221+
222+
Caused by:
223+
0: Important work failed while reading database
224+
1: Reading database failure
225+
2: invalid digit found in string
226+
227+
228+
Error pretty:
229+
Error {
230+
context: "Service could not do the important work",
231+
source: Error {
232+
context: "Important work failed while reading database",
233+
source: Error {
234+
context: "Reading database failure",
235+
source: ParseIntError {
236+
kind: InvalidDigit,
237+
},
238+
},
239+
},
240+
}
241+
```
242+
243+
## Consequences
244+
245+
* We have defined the following aliases that should be used by default:
246+
* `StdResult`: the default result that should be returned by a function (unless a more specific type is required).
247+
* `StdError`: the default error that should be used (unless a more specific type is required).
248+
249+
```rust
250+
/* Code extracted from mithril-common::lib.rs */
251+
/// Generic error type
252+
pub type StdError = anyhow::Error;
253+
254+
/// Generic result type
255+
pub type StdResult<T> = anyhow::Result<T, StdError>;
256+
```
257+
258+
* The function that returns an error from a sub-system should systematically add a context to the error with the `with_context` method, in order to provide clear stack traces and ease debugging.
259+
260+
* When printing an `StdError` we should use the debug format without the pretty modifier, ie:
261+
```rust
262+
println!("Error debug:\n {error:?}\n\n");
263+
```
264+
265+
* When wrapping an error in a `thiserror` enum variant we should use the `source` attribute that will provide a clearer stack trace:
266+
```rust
267+
/// Correct usage with `source` attribute
268+
#[derive(Error, Debug)]
269+
pub enum DomainError {
270+
#[error("Anyhow error")]
271+
AnyhowWrapWithSource(#[source] StdError),
272+
}
273+
```
274+
275+
```rust
276+
/// Incorrect usage without `source` attribute
277+
#[derive(Error, Debug)]
278+
pub enum DomainError {
279+
#[error("Anyhow error: {0}")]
280+
AnyhowWrapWithoutAnything(StdError),
281+
}
282+
```
283+
284+
* Here are some tips on how to discriminate between creating a new error using `thiserror` or using an `StdResult`:
285+
* If you raise an anyhow error which only contains a string this means that you are creating a new error that doesn't come from a sub-system. In that case you should create a type using `thiserror` intead, ie:
286+
```rust
287+
// Avoid
288+
return Err(anyhow!("my new error"));
289+
290+
// Prefer
291+
#[derive(Debug,Error)]
292+
pub enum MyError {
293+
MyNewError
294+
}
295+
return Err(MyError::MyNewError);
296+
```
297+
298+
* (*Still undecided*) You should avoid wrapping a `StdError` in a `thiserror` type. This __breaks__ the stack trace and makes it really difficult to retrieve the innermost errors using `downcast_ref`. When the `thiserror` type is itself wrapped in a `StdError` afterward, you would have to `downcast_ref` twice: first to get the `thiserror` type and then to get the innermost error.
299+
This should be restricted to the topmost errors of our system (ie the state machine errors).

0 commit comments

Comments
 (0)