Stream: project-error-handling

Topic: methods that consume self


view this post on Zulip Gus Gutoski (Apr 02 2021 at 10:45):

Hi folks. Any recommendations for the following? I have a struct method that consumes self and returns Self, kinda like a builder pattern:

impl Foo {
  fn consume_self(self) -> Self {
    // lots of important stuff
    self
}

Now I want to change consume_self so that it might fail. For example, let's suppose that consume_self needs to deserialize something. A naive design might do this:

fn consume_self(self, bytes: &[u8]) -> bincode::Result<Self> {
  let data = bincode::deserialize(bytes)?;
  // do something important with data
  Ok(self)
}

Problem: if there's an error then I lose my self, which is bad. I can think of several solutions but none of them is a knock-down win. Any recommendations?

view this post on Zulip oliver (Apr 02 2021 at 10:51):

what is your top option currently?

view this post on Zulip Gus Gutoski (Apr 02 2021 at 10:58):

One idea is to bundle Self into the returned error:

fn consume_self(self, bytes: &[u8]) -> Result<Self, (Self, bincode::Error)> {
  let res = bincode::deserialize(bytes);
  if res.is_err() {
    return Err((self, res.unwrap_err()));
  }
  // important stuff
  self
}

This way I can recover self even if there's an error. It's a bit ugly and I don't know yet how ergonomic it is for the caller.

view this post on Zulip Gus Gutoski (Apr 02 2021 at 11:02):

Another idea is to return a tuple:

fn consume_self(self, bytes: &[u8]) -> (Self, Result<(), bincode::Error>) {
  let res = bincode::deserialize(bytes);
  if res.is_err() {
    return (self, res);
  }
  // important stuff
  (self, Ok(()))
}

view this post on Zulip Gus Gutoski (Apr 02 2021 at 11:12):

I have complete control over the design. Another more exotic idea is to add a Result member to Foo and rely on the user to check it:

struct Foo {
  // important fields
  res: Result<(),bincode::Error>,
}
impl Foo {
  fn consume_self(mut self, bytes: &[u8]) -> Self {
    self.res = bincode::deserialize(bytes);
    // handle error and other important stuff
    self
    }
}

view this post on Zulip Mario Carneiro (Apr 02 2021 at 11:20):

Does it have to be a builder pattern function? You might also do foo(&mut self, bytes: &[u8]) -> Result<(), bincode::Error>

view this post on Zulip Mario Carneiro (Apr 02 2021 at 11:22):

if you just want builder pattern for ergonomic reasons, I would stick to the "naive" version that returns bincode::Result<Self>, and use the &mut self version for applications that don't want to lose self even in case of an error

view this post on Zulip Gus Gutoski (Apr 02 2021 at 11:31):

Thanks for the comment! In my application the user will always want to keep self in case of error. I'm building a finite state machine that uses Rust's type system to enforce state transitions, so I really want to consume self. A failed deserialize is really just another state transition. Another idea I did not explain due to complexity is to modify the state machine design to handle the error that way. Feel free to post more ideas!

view this post on Zulip Mario Carneiro (Apr 02 2021 at 11:45):

I'm building a finite state machine that uses Rust's type system to enforce state transitions, so I really want to consume self. A failed deserialize is really just another state transition.

In that case, it sounds like you want the Result<Self, (Self, bincode::Error)> version, since you can use special types in the two branches to encode the typestate

view this post on Zulip Mario Carneiro (Apr 02 2021 at 11:46):

in particular, instead of (Self, bincode::Error) it could be ErrorType which is a newtype struct around (Self, bincode::Error) or the parts of Self that are still valid

view this post on Zulip Jane Lusby (Apr 02 2021 at 13:39):

Yea, I was gonna say this sounds like how downcast uses result

view this post on Zulip Jane Lusby (Apr 02 2021 at 13:39):

Where it either returns the concrete type of it succeeds or the original box if it fails

view this post on Zulip Jane Lusby (Apr 02 2021 at 13:39):

Not an uncommon pattern in std

view this post on Zulip Jane Lusby (Apr 02 2021 at 13:40):

I think hashmap has a similar api that wraps the key and value in an error type

view this post on Zulip Jane Lusby (Apr 02 2021 at 13:40):

So if you need to return an informative error type you might want to follow they example instead


Last updated: Jan 26 2022 at 14:20 UTC