Here's how I tend to do it already: defer the expensive parts until the implicit into
and/or until you actually display things. And From<(SrcErrWithContext, &CheapThing)>
is often a part of that.
Adapted from a project:
pub struct FileReadError {
path: PathBuf,
kind: FileReadErrorKind,
}
enum FileReadErrorKind {
Open(io::Error), // io::Error is too general, so these
Read(io::Error), // supply more context
Parse(FileLineError),
}
The expensive parts:
impl From<(FileReadErrorKind, &Path)> for FileReadError {
fn from((kind, path): (FileReadErrorKind, &Path)) -> Self {
let path = path.to_owned();
Self { path, kind }
}
}
impl Error for FileReadError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
// (return the io::Error etc .. xor include them in Display below)
}
}
impl fmt::Display for FileReadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use FileReadErrorKind as Kind;
match self.kind {
Kind::Open(_) => write!(f, "could not open {} for reading", self.path.display()),
// ...
}
}
}
What using the infrastructure looks like:
pub fn open(path: &Path) -> Result<Self, FileReadError> {
use FileReadErrorKind as Kind;
// Today / with zip_err
let file = File::open(path).map_err(|e| (Kind::Open(e), path))?;
let file = File::open(path).map_err(Kind::Open).zip_err(path)?;
// ...
// Today / with zip_err
this.read_inner(file).map_err(|e| (e, path))?;
this.read_inner(file).zip_err(path)?;
Ok(this)
}
fn read_inner(&mut self, mut file: File) -> Result<(), FileReadErrorKind> {
// ...
}