Reraising Errors from foreign code

Hi y'all

Yesterday I was working with a friend on an embedded crate.
The crate has to handle some pins on unknown hardware. The only thing we know is that the pins implement

Trait embedded_hal::digital::v2::OutputPin

pub trait OutputPin {
    type Error;
    fn set_low(&mut self) -> Result<(), Self::Error>;
    fn set_high(&mut self) -> Result<(), Self::Error>;

    fn set_state(&mut self, state: PinState) -> Result<(), Self::Error> { ... }
}

Single digital push-pull output pin

The crate itself uses these pins to abstract outputs on a shift register as OutputPins. The shift register is connected via the pins on the unknown hardware that also implement OuputPin.

I receive errors of type <hardwarepin as OutputPin>::Error and have to reraise them as <shiftregisterpin as OutputPin>::Error. Currently we just do this with map_error(|_e| ())? because we cannot know what kind of error the concrete hardware pins may raise.(
Probably none, because I don't know of any hardware where the pin could fail to set, but that's besides the point.)

My favourite solution would have been to just "reraise" the error happening on the pin. But I don't really see how to do this in a sensible manner. If OutputPin::Error had a trait bound of std::error::Error I could at least raise a Box<dyn std::error::Error> but that's not the case. (Besides the fact, that microchips usually work without an allocator, so no Box at all)

So now I have two questions

  1. Is there another way to just "reraise" errors of unknown type? Beware that the different pins may raise different errors.
  2. Do you often use std::error::Error? I haven't seen it in the wild yet but I also haven't looked particularily hard yet.

I guess since there are three pins for the connection and I have the trait bounds

where
    Pin1: OutputPin,
    Pin2: OutputPin,
    Pin3: OutputPin,

I could have an Error struct that just combines three error types. It would look something like this

enum ShiftregisterError<E1, E2, E3> {
    Pin1(E1),
    Pin2(E2),
    Pin3(E3),
}
...

impl<Pin1, Pin2, Pin3> OutputPin for ShifregisterPin<Pin1, Pin2, Pin3>
where
    Pin1: OutputPin,
    Pin2: OutputPin,
    Pin3: OutputPin,
{
    fn set_low() -> Result<(), ShiftRegisterError<Pin1::Error, Pin2:Error, Pin3::Error> {
        pin1.set_low().map_error(|e| ShiftregisterError::Pin1(e))?;
....
        pin2.set_high().map_error(|e| ShiftregisterError::Pin2(e))?;
    }
}

Instead of using Box<dyn Error>, why not make your error type generic?

enum ShiftRegisterPinError<E> {
  /// An error raised when writing to the underlying hardware pin fails.
  Pin(E),
  ...
}

impl<P: OutputPin> ShiftRegister<P> {
  fn do_stuff(&mut self) -> Result<(), ShiftRegisterPinError<P::Error>> { 
    self.pin.set_high().map_err(ShiftRegisterPinError::Pin)?;
    ... 
  }
}

Otherwise if you want to retain all the context without infecting the rest of your code with the hardware pin's error type, E, you'll need to use some form of type erasure like Box<dyn ...> (e.g. Box<dyn Display> if you don't have access to std::error::Error).

Yeah that's basically the same as I proposed above. The reason why I included multiple pins is because all three pins are used to set a single Pin on the Shiftregister.
Using Display instead of std::error::Error might be interesting though. But doesn't that still have the problem that I need to restrict Pinx::Error: Display but not all OutputPin::Error necesarily implement that? And people using this crate usually won't have controll over that.

I would think that most error types implement will Display, anyway. Otherwise you could find another useful trait like Debug.

1 Like

Yeah I guess that's true. Thanks for the help.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.