Hi,
I had a few questions about error handling when implementing a trait in Rust.
I'm writing a library around hypervisors to wrap their "introspection APIs", meaning an API to query the virtual hardware state from the VMs and listen for hardware events such as interrupts or pagefaults.
The library provides multiple drivers, which can be enabled/disabled at build time:
- Xen
- KVM
- VirtualBox
etc..
All of these drivers implement the same trait:
pub trait Introspectable {
// read VCPU registers
fn read_registers(&self, _vcpu: u16) -> Result<Registers, Box<dyn Error>> { unimplemented!(); }
...
}
Our naïve implements used Box<dyn Error>
, but this is not the best approach, as I understand, so I had a couple of solutions to implement proper error handling:
Solution 1 - returning an Error trait
Basically declare an Error trait, deriving from std::error::Error
, to keep a good level of abstraction from the API and the drivers implementation.
pub trait DriverError: Error {}
Update our Introspectable
trait functions definition:
pub trait Introspectable {
fn read_registers(&self, _vcpu: u16) -> Result<Registers, Box<dyn DriverError>> { unimplemented!(); }
}
Implement a custom error in each driver, based on thiserror
, and implement the from
trait to convert them to the Box<dyn DriverError>
type:
// defining custom Xen driver error
[derive(thiserror::Error, Debug)]
pub enum XenDriverError {
#[error("no pending event channel ports")]
NoPendingChannel,
#[error(transparent)]
TryFromIntError(#[from] TryFromIntError),
#[error(transparent)]
IoError(#[from] IoError),
#[error(transparent)]
XcError(#[from] XcError),
#[error(transparent)]
NixError(#[from] nix::Error),
#[error(transparent)]
ForeignMemoryError(#[from] XenForeignMemoryError),
}
impl DriverError for XenDriverError {}
impl From<XenDriverError> for Box<dyn DriverError> {
fn from(err: XenDriverError) -> Box<dyn DriverError> {
Box::new(err)
}
}
impl Introspectable for Xen {
fn read_registers(&self, vcpu: u16) -> Result<Registers, Box<dyn DriverError>> {
...
.map_err(XenDriverError::from)?;
}
}
Solution 2 - Returning an Enum error
I also watched this talk about error handling:
"RustConf 2020 - Error handling Isn't All About Errors by Jane Lusby" by @yaahc
"We don't know of users will handle our errors, ... , so we need error type that are maximaly flexible, ... and we want our errors to be an enum, so they can be reacted to easily"
Therefore I implement this solution too:
#[derive(thiserror::Error, Debug)]
pub enum DriverError {
#[cfg(feature = "xen")]
#[error(transparent)]
Xen(#[from] XenDriverError),
}
pub trait Introspectable {
fn read_registers(&self, _vcpu: u16) -> Result<Registers, DriverError> { unimplemented!(); }
}
And in the Xen driver:
#[derive(thiserror::Error, Debug)]
pub enum XenDriverError {
#[error("no pending event channel ports")]
NoPendingChannel,
#[error(transparent)]
TryFromIntError(#[from] TryFromIntError),
#[error(transparent)]
IoError(#[from] IoError),
#[error(transparent)]
XcError(#[from] XcError),
#[error(transparent)]
NixError(#[from] nix::Error),
#[error(transparent)]
ForeignMemoryError(#[from] XenForeignMemoryError),
}
impl Introspectable for Xen {
fn read_registers(&self, vcpu: u16) -> Result<Registers, DriverError> {
...
.map_err(|err| DriverError::from(XenDriverError::NoPendingChannel(err)))?
}
}
So I'm confused about which way to go, and I'm looking for guidance about what is expected from a library, in terms of errors types, in 2021 rust ?
- Error enum, because it is well defined, but the downside is that the
DriverError
is composed of multiple kind of driver specific error, so we tend to break the abstraction here ? (just my feeling) - Error trait, which keeps a good level of abstraction from the driver implementation, but at the same time, what about the user error handling ?
- What about API stability ? as I understand in the case of enum Errors, using
#[non_exhaustive]
does the trick - last point: performance: dynamic downcasting has a bit of a runtime cost, and i would like to avoid that if possible.
Thank you for your help !