error_context/
lib.rs

1/*!
2This crate provides methods and types that help with adding additional context information to error types.
3
4# Usage
5There are two ways to add context information to your error types:
61. by extending your error type with a filed that will store the context information and then adding context to the error value,
72. wrapping any error type together with the context information and then converting this boundle to type that can store the error and context.
8
9This crate provides types, traits and extension methods designed to help with the above tasks.
10It is recommended to import all the types and traits via perlude module: `use error_type::prelude::*`.
11
12## Adding context to types that can collect context
13If your type can collect context information you can implement `WithContext` trait for it. By doing so you enable some of the provided extension methods to work with your type.
14
15### Directly to value
16You can add context to value of your error with `.with_context(context)`.
17
18### To error wrapped in `Result`
19Use `.error_while(context)` method on `Result` value to add context to error value of type that implements `WithContext`.
20
21You can also use `in_context_of(context, closure)` function to add context to result of provided closure. You can use `?` within the closure to control the flow.
22
23There is also `.error_while_with(context_function)` and `in_context_of_with(context_function, closure)` variants that can be used to defer construction of context to error path.
24
25## Adding context to other types
26External error types may not support adding context.
27The `ErrorContext` type can be used to wrap error value and context information together. 
28This type implements `WithContext` and adding further context information will result in wrapping with another layer of `ErrorContext` type.
29
30The main use case for this method is to wrap error in one or more layers of context and then convert them to your own error type consuming 
31the error and the context information using `From` trait.
32This enables use of `?` to convert external error types with added context to your error type.
33
34### Directly to value
35You can wrap any type in `ErrorContext` type using `.wrap_context(context)` method.
36
37### To error wrapped in `Result`
38When working with `Result` value you can wrap error value in `ErrorContext` using `.wrap_error_while(context)`.
39
40There is also `.wrap_error_while_with(context_function)` and `wrap_in_context_of_with(context_function, closure)` variants that can be used to defer construction of context to error path.
41
42### Using `ErrorNoContext`
43You can also use `.to_root_cause()` directly on error value or `.map_error_context()` on `Result` to wrap error type in `ErrorNoContext`.
44
45Adding context information to `ErrorNoContext` converts it into `ErrorContext`. 
46`ErrorNoContext` is intended to be used within function scope to enable functions and methods that work with `WithContext` to add 
47context information bafore error is returned.
48
49## Usage example
50In this example we will create our own error type called `MyError`.
51We will wrap extra context information to `std::io::Error` value using `.wrap_error_while(context)` and as another example using `.wrap_in_context_of(context, closure)`.
52Finally by implementing `From<ErrorContext<io::Error, &'static str>>` for `MyError` we can use `?` operator to convert this error to `MyError` 
53persisting the context information added.
54
55```rust
56use error_context::prelude::*;
57use std::io;
58
59enum MyError {
60    IoError(io::Error, &'static str),
61}
62
63impl From<ErrorContext<io::Error, &'static str>> for MyError {
64    fn from(error: ErrorContext<io::Error, &'static str>) -> MyError {
65        MyError::IoError(error.error, error.context)
66    }
67}
68
69fn work_with_file() -> Result<(), MyError> {
70    Err(io::Error::new(io::ErrorKind::InvalidInput, "boom!"))
71        .wrap_error_while("working with file")?;
72    Ok(())
73}
74
75match work_with_file().unwrap_err() {
76    MyError::IoError(_, "working with file") => (),
77    _ => panic!("wrong context"),
78}
79
80fn do_stuff() -> Result<(), MyError> {
81    wrap_in_context_of("doing stuff", || {
82        Err(io::Error::new(io::ErrorKind::InvalidInput, "boom!"))?;
83        Ok(())
84    })?;
85    Ok(())
86}
87
88match do_stuff().unwrap_err() {
89    MyError::IoError(_, "doing stuff") => (),
90    _ => panic!("wrong context"),
91}
92```
93
94# Usage guidelines
95* Use error context to provide information about which good program path was taken that lead to an error, e.g: "while parsing filed x of message type y".
96* Error context should provide detail for the end user who sees the error message and not be used to distinguish between two different errors in code - use `Display` types like `&'static str` as context type.
97* Don't add errors or error path information to context - this should be part of the error type, in particular its `Display` and `Error::source` implementation.
98* Don't add arguments of function call you are rising error from to the context - this should be responsibility of the caller - otherwise it would be difficult to
99avoid non-`'static` references or allocations on error path and avoid showing sensitive data to end user, e.g. SQL query text or passwords.
100* Don't put non-`'static` references to context or the error value cannot be bubbled up easily or returned as `Error::source`.
101*/
102
103use std::error::Error;
104use std::fmt::Debug;
105use std::fmt::{self, Display};
106
107/// Includes `WithContext` trait, `ErrorContext`, `ErrorNoContext` types and related conversion traits and `*in_context_of*` functions
108pub mod prelude {
109    pub use crate::{
110        in_context_of, in_context_of_with, wrap_in_context_of, wrap_in_context_of_with,
111        ErrorContext, ErrorNoContext, MapErrorNoContext, ResultErrorWhile, ResultErrorWhileWrap,
112        ToErrorNoContext, WithContext, WrapContext,
113    };
114}
115
116/// Add context to object
117pub trait WithContext<C> {
118    type ContextError;
119    fn with_context(self, context: C) -> Self::ContextError;
120}
121
122/// Add context to error carried by another type like `Result`
123pub trait ResultErrorWhile<C> {
124    type ContextError;
125    fn error_while(self, context: C) -> Self::ContextError;
126    fn error_while_with<F>(self, context: F) -> Self::ContextError
127    where
128        F: FnOnce() -> C;
129}
130
131impl<O, E, C> ResultErrorWhile<C> for Result<O, E>
132where
133    E: WithContext<C, ContextError = E>,
134{
135    type ContextError = Self;
136    fn error_while(self, context: C) -> Self {
137        self.map_err(|e| e.with_context(context))
138    }
139
140    fn error_while_with<F>(self, context: F) -> Self::ContextError
141    where
142        F: FnOnce() -> C,
143    {
144        self.map_err(|e| e.with_context(context()))
145    }
146}
147
148/// Wrap value in `ErrorNoContext` to add more context using `WithContext` trait that will convert it to `ErrorContext`
149#[derive(Debug)]
150pub struct ErrorNoContext<E>(pub E);
151
152impl<E> Display for ErrorNoContext<E>
153where
154    E: Display,
155{
156    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157        self.0.fmt(f)
158    }
159}
160
161impl<E> Error for ErrorNoContext<E>
162where
163    E: Error,
164{
165    fn description(&self) -> &str {
166        self.0.description()
167    }
168
169    fn source(&self) -> Option<&(dyn Error + 'static)> {
170        self.0.source()
171    }
172}
173
174impl<E, C> WithContext<C> for ErrorNoContext<E> {
175    type ContextError = ErrorContext<E, C>;
176    fn with_context(self, context: C) -> ErrorContext<E, C> {
177        ErrorContext {
178            error: self.0,
179            context,
180        }
181    }
182}
183
184/// Wrap value with `ErrorNoContext`
185pub trait ToErrorNoContext<T> {
186    fn to_root_cause(self) -> ErrorNoContext<T>;
187}
188
189impl<T> ToErrorNoContext<T> for T {
190    fn to_root_cause(self) -> ErrorNoContext<Self> {
191        ErrorNoContext(self)
192    }
193}
194
195/// Map error caring type by wrapping it's error value in `ErrorNoContext`
196pub trait MapErrorNoContext<O, E> {
197    fn map_error_context(self) -> Result<O, ErrorNoContext<E>>;
198}
199
200impl<O, E> MapErrorNoContext<O, E> for Result<O, E> {
201    fn map_error_context(self) -> Result<O, ErrorNoContext<E>> {
202        self.map_err(ToErrorNoContext::to_root_cause)
203    }
204}
205
206/// Wrap error value together with context information
207#[derive(Debug)]
208pub struct ErrorContext<E, C> {
209    pub error: E,
210    pub context: C,
211}
212
213impl<E, C> Display for ErrorContext<E, C>
214where
215    E: Display,
216    C: Display,
217{
218    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
219        write!(f, "while {} got error: {}", self.context, self.error)
220    }
221}
222
223impl<E, C> Error for ErrorContext<E, C>
224where
225    E: Error,
226    C: Display + Debug,
227{
228    fn description(&self) -> &str {
229        self.error.description()
230    }
231
232    fn source(&self) -> Option<&(dyn Error + 'static)> {
233        self.error.source()
234    }
235}
236
237impl<E, C, C2> WithContext<C2> for ErrorContext<E, C> {
238    type ContextError = ErrorContext<ErrorContext<E, C>, C2>;
239    fn with_context(self, context: C2) -> ErrorContext<ErrorContext<E, C>, C2> {
240        ErrorContext {
241            error: self,
242            context,
243        }
244    }
245}
246
247/// Wrap value in type with context information
248pub trait WrapContext<C> {
249    type ContextError;
250    fn wrap_context(self, context: C) -> Self::ContextError;
251}
252
253impl<E, C> WrapContext<C> for E {
254    type ContextError = ErrorContext<E, C>;
255    fn wrap_context(self, context: C) -> ErrorContext<E, C> {
256        ErrorContext {
257            error: self,
258            context,
259        }
260    }
261}
262
263/// `Result` extension trait to wrap error value in `ErrorContext` with given context information
264pub trait ResultErrorWhileWrap<O, E, C> {
265    fn wrap_error_while(self, context: C) -> Result<O, ErrorContext<E, C>>;
266    fn wrap_error_while_with<F>(self, context: F) -> Result<O, ErrorContext<E, C>>
267    where
268        F: FnOnce() -> C;
269}
270
271impl<O, E, C> ResultErrorWhileWrap<O, E, C> for Result<O, E>
272where
273    E: WrapContext<C, ContextError = ErrorContext<E, C>>,
274{
275    fn wrap_error_while(self, context: C) -> Result<O, ErrorContext<E, C>> {
276        self.map_err(|e| e.wrap_context(context))
277    }
278
279    fn wrap_error_while_with<F>(self, context: F) -> Result<O, ErrorContext<E, C>>
280    where
281        F: FnOnce() -> C,
282    {
283        self.map_err(|e| e.wrap_context(context()))
284    }
285}
286
287/// Executes closure adding context to returned error value with `.with_context(context)`
288pub fn in_context_of<O, E, C, CE, B>(context: C, body: B) -> Result<O, CE>
289where
290    E: WithContext<C, ContextError = CE>,
291    B: FnOnce() -> Result<O, E>,
292{
293    body().map_err(|e| e.with_context(context))
294}
295
296/// Executes closure adding context to returned error value with `.with_context(context)` obtaining context by calling given function on error path
297pub fn in_context_of_with<O, E, C, CE, F, M, B>(context: F, body: B) -> Result<O, CE>
298where
299    F: FnOnce() -> C,
300    E: WithContext<C, ContextError = CE>,
301    B: FnOnce() -> Result<O, E>,
302{
303    body().map_err(|e| e.with_context(context()))
304}
305
306/// Executes closure adding context to returned error value by wrapping it in `ErrorContext` with `.wrap_context(context)`
307pub fn wrap_in_context_of<O, E, C, B>(context: C, body: B) -> Result<O, ErrorContext<E, C>>
308where
309    E: WrapContext<C, ContextError = ErrorContext<E, C>>,
310    B: FnOnce() -> Result<O, E>,
311{
312    body().map_err(|e| e.wrap_context(context))
313}
314
315/// Executes closure adding context to returned error value by wrapping it in `ErrorContext` with `.wrap_context(context)` obtaining context by calling given function on error path
316pub fn wrap_in_context_of_with<O, E, C, F, B>(
317    context: F,
318    body: B,
319) -> Result<O, ErrorContext<E, C>>
320where
321    F: FnOnce() -> C,
322    E: WrapContext<C, ContextError = ErrorContext<E, C>>,
323    B: FnOnce() -> Result<O, E>,
324{
325    body().map_err(|e| e.wrap_context(context()))
326}
327
328#[cfg(test)]
329mod tests {
330    use super::prelude::*;
331    use assert_matches::*;
332    use std::io;
333
334    #[derive(Debug)]
335    enum FooError {
336        Foo {
337            context: Vec<String>,
338        },
339        Bar {
340            num: i32,
341            context: Vec<String>,
342        },
343        IoError {
344            error: io::Error,
345            context: Vec<String>,
346        },
347    }
348
349    impl WithContext<String> for FooError {
350        type ContextError = Self;
351        fn with_context(mut self, message: String) -> Self {
352            match self {
353                FooError::Foo {
354                    ref mut context, ..
355                } => context.push(message),
356                FooError::Bar {
357                    ref mut context, ..
358                } => context.push(message),
359                FooError::IoError {
360                    ref mut context, ..
361                } => context.push(message),
362            }
363            self
364        }
365    }
366
367    impl From<ErrorContext<io::Error, String>> for FooError {
368        fn from(error_context: ErrorContext<io::Error, String>) -> FooError {
369            FooError::IoError {
370                error: error_context.error,
371                context: vec![error_context.context],
372            }
373        }
374    }
375
376    #[test]
377    fn test_in_type_context() {
378        let err: Result<(), FooError> = Err(FooError::Foo {
379            context: Vec::new(),
380        });
381        assert_matches!(err.error_while("doing stuff".to_string()), Err(FooError::Foo { context }) => assert_eq!(context, vec!["doing stuff".to_string()]));
382
383        let err: Result<(), FooError> = Err(FooError::Bar {
384            num: 1,
385            context: Vec::new(),
386        });
387        assert_matches!(err.error_while("doing stuff".to_string()), Err(FooError::Bar { num: 1, context }) => assert_eq!(context, vec!["doing stuff".to_string()]));
388    }
389
390    #[test]
391    fn test_wrapped_context() {
392        use std::io::{Error, ErrorKind};
393        let err: Result<(), Error> = Err(Error::new(ErrorKind::Other, "oh no!"));
394
395        assert_eq!(
396            err.wrap_error_while("doing stuff".to_string())
397                .unwrap_err()
398                .to_string(),
399            "while doing stuff got error: oh no!"
400        );
401    }
402
403    #[test]
404    fn test_wrapped_context_nested() {
405        use std::io::{Error, ErrorKind};
406        let err: Result<(), Error> = Err(Error::new(ErrorKind::Other, "file is no good"));
407
408        assert_eq!(
409            err.wrap_error_while("opening file".to_string())
410                .wrap_error_while("processing fish sticks".to_string())
411                .unwrap_err()
412                .to_string(),
413            "while processing fish sticks got error: while opening file got error: file is no good"
414        );
415    }
416
417    #[test]
418    fn test_in_context_of_type_context() {
419        let err = in_context_of("doing stuff".to_string(), || {
420            let err: Result<(), FooError> = Err(FooError::Foo {
421                context: Vec::new(),
422            });
423            err
424        });
425
426        assert_matches!(err.error_while("doing other stuff".to_string()), Err(FooError::Foo { context: c }) => assert_eq!(c, vec!["doing stuff".to_string(), "doing other stuff".to_string()]));
427    }
428
429    #[test]
430    fn test_wrap_in_context_of_type_context() {
431        fn foo() -> Result<(), FooError> {
432            wrap_in_context_of("doing stuff".to_string(), || {
433                Err(io::Error::new(io::ErrorKind::InvalidInput, "boom!"))?;
434                Ok(())
435            })?;
436            Ok(())
437        }
438
439        assert_matches!(foo().error_while("doing other stuff".to_string()), Err(FooError::IoError { context, .. }) => assert_eq!(context, vec!["doing stuff".to_string(), "doing other stuff".to_string()]));
440    }
441
442    #[test]
443    fn test_in_context_of_wrapped_context() {
444        use std::io::{Error, ErrorKind};
445
446        let err = in_context_of("opening file".to_string(), || {
447            let err: Result<(), Error> = Err(Error::new(ErrorKind::Other, "file is no good"));
448            err.map_error_context()
449        });
450
451        assert_eq!(
452            err.wrap_error_while("processing fish sticks".to_string())
453                .unwrap_err()
454                .to_string(),
455            "while processing fish sticks got error: while opening file got error: file is no good"
456        );
457    }
458}