error-stack: Putting data in the context struct vs. using attachments? #1818
-
Hi! I love this library, and one thing I'm struggling to understand is attachments, and when they're useful. I'm hoping the following example can explain my confusion, and then you can hopefully show me what I'm missing. Contrived ExampleLet's say we take the experiment parsing example from the README and change it to have the #[derive(Debug)]
struct ExperimentError {
reason: String
};
impl fmt::Display for ExperimentError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("experiment error: could not run experiment because {}", self.reason)
}
} then for an example like this let experiment = parse_experiment(description)
.attach_printable(format!("experiment {exp_id} could not be parsed"))
.change_context(ExperimentError)?; you'd instead write the following let experiment = parse_experiment(description)
.change_context_lazy(|| ExperimentError { reason: format!("experiment {exp_id} could not be parsed" })?; and you don't even need an attachment at all. The only difference as far as I can tell is that the error will render slightly differently. Moreover there seem to be some benefits to doing it this way:
Going overboardThis is a bit impractical in its granularity, but anyways with #[derive(Error)]
pub enum ExperimentErrorReason {
#[error("experiment {0} could not be parsed")]
CouldNotBeParsed(usize),
// Other errors
}
#[derive(Debug, Error)]
#[error("experiment error: could not run experiment because {reason}")]
struct ExperimentError {
reason: ExperimentErrorReason
};
let experiment = parse_experiment(description)
.change_context_lazy(|| ExperimentError { reason: ExperimentErrorReason::CouldNotBeParsed(exp_id) })?; My Question...The overarching point I'm trying to demonstrate is that I don't exactly understand why 99% of attachments aren't just a part of the context struct. The only case I can see for attachments over putting stuff into the context struct is attaching stuff that's optional because you don't feel like putting an Could you explain if there's something I'm missing here? Maybe some real-world examples from HASH would help demonstrate the utility of attachments better |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Hello, I am sorry for taking so long to respond to your question! I completely missed this thread. My bad 😅 Your question is entirely valid, and thanks for asking. I think "blank" structs with no information and your use-case go hand in hand, but there are some use cases where attachments are beneficial. They are primarily useful in cases where either A) things might change, B) information is unknown during the creation of the context, C) information is shared across multiple errors I am working on an experimental deserialization library ( A) Things might changeLet's say we want to create a new data type ( use deer::{
error::{DeserializeError, ExpectedType, ReceivedValue, ValueError, Variant},
Deserialize, Deserializer, Document, Reflection, Schema,
};
use error_stack::Report;
#[allow(non_camel_case_types)]
pub struct u5(u8);
impl Reflection for u5 {
fn schema(_: &mut Document) -> Schema {
Schema::new("integer")
.with("maximum", 31)
.with("minimum", 0)
}
}
impl<'de> Deserialize<'de> for u5 {
type Reflection = Self;
fn deserialize<D: Deserializer<'de>>(de: D) -> error_stack::Result<Self, DeserializeError> {
match u8::deserialize(de) {
Ok(value) if value > 31 => Err(Report::new(ValueError.into_error())
.attach(ReceivedValue::new(value))
.attach(ExpectedType::new(Self::reflection()))
.change_context(DeserializeError)),
Ok(value) => Ok(u5(value)),
Err(mut error) => {
for frame in error
.frames_mut()
.filter_map(|frame| frame.downcast_mut::<ExpectedType>())
{
*frame = ExpectedType::new(Self::reflection());
}
Err(error)
}
}
}
} as you can see here, we can make full use of the existing deserialization logic for This is of course not applicable to all information, so for information specific to an error that is not expected to change during its lifetime, just using a struct with values is 100% okay and encouraged! B) Information is unknownAnother example from Let's say we have a json struct like this
The json path to the error might be: graph RL;
V1["Value Error"]
L1["Location::Entry(b) | attached in HashMapVisitor"]
L2["Location::Array(2) | attached in ArrayVisitor"]
L3["Location::Entry(a) | attached in HashMapVisitor"]
L4["Location::Array(3) | attached in ArrayVisitor"]
V1 --> L1
L1 --> L2
L2 --> L3
L3 --> L4
But there is other information that might be helpful, regardless of the error present, like what was the database connection that caused the issue. This might not be available at the creation site, but at a later date, this also saves a lot of boilerplates, instead of making sure that every single error struct has a specific value of a type (which is easy to miss), we can just attach the information when we come across the issue at the application layer without worrying about the underlying error. C) Information is shared amongst multiple errorssince 0.2, A graph RL;
C8 --> A7 --> A6 --> C5 --> A4 --> A3 --> A2 --> C1
but can instead be graph RL;
C8 --> A7 --> A6 --> A2 --> C1
C5 --> A4 --> A3 --> A2
This attachment |
Beta Was this translation helpful? Give feedback.
Hello, I am sorry for taking so long to respond to your question! I completely missed this thread. My bad 😅
Your question is entirely valid, and thanks for asking. I think "blank" structs with no information and your use-case go hand in hand, but there are some use cases where attachments are beneficial. They are primarily useful in cases where either A) things might change, B) information is unknown during the creation of the context, C) information is shared across multiple errors
I am working on an experimental deserialization library (
deer
), which heavily useserror-stack
. To illustrate the use cases outlined I'll take some snippets of the codeA) Things might change
Let's say we want…