Rust Error Enum with Source Chains
Model configuration failures with a custom Rust error enum, Display and Error implementations, From conversion, and preserved parse causes.
Code
use std::error::Error;
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Missing(&'static str),
Malformed { detail: String },
InvalidPort { value: String, source: ParseIntError },
OutOfRange(u16),
Io(io::Error),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::Missing(key) => write!(f, "missing required key `{key}`"),
ConfigError::Malformed { detail } => write!(f, "malformed config: {detail}"),
ConfigError::InvalidPort { value, .. } => write!(f, "invalid port `{value}`"),
ConfigError::OutOfRange(port) => write!(f, "port {port} must be between 1 and 65535"),
ConfigError::Io(err) => write!(f, "io error: {err}"),
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConfigError::InvalidPort { source, .. } => Some(source),
ConfigError::Io(err) => Some(err),
ConfigError::Missing(_) | ConfigError::Malformed { .. } | ConfigError::OutOfRange(_) => None,
}
}
}
impl From<io::Error> for ConfigError {
fn from(err: io::Error) -> Self {
ConfigError::Io(err)
}
}
#[derive(Debug, PartialEq)]
struct Config {
host: String,
port: u16,
}
fn parse_config(raw: &str) -> Result<Config, ConfigError> {
let mut host = None;
let mut port = None;
for raw_line in raw.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (key, value) = line
.split_once('=')
.ok_or_else(|| ConfigError::Malformed { detail: format!("expected key=value, got `{line}`") })?;
let key = key.trim();
let value = value.trim();
match key {
"host" => {
if value.is_empty() {
return Err(ConfigError::Missing("host"));
}
host = Some(value.to_string());
}
"port" => {
let parsed = value
.parse::<u16>()
.map_err(|source| ConfigError::InvalidPort { value: value.to_string(), source })?;
if parsed == 0 {
return Err(ConfigError::OutOfRange(parsed));
}
port = Some(parsed);
}
other => {
return Err(ConfigError::Malformed { detail: format!("unsupported key `{other}`") });
}
}
}
Ok(Config {
host: host.ok_or(ConfigError::Missing("host"))?,
port: port.ok_or(ConfigError::Missing("port"))?,
})
}
fn load(path: &str) -> Result<Config, ConfigError> {
let raw = std::fs::read_to_string(path)?;
parse_config(&raw)
}
fn main() {
let valid = "host = 127.0.0.1\nport = 8080\n";
println!("valid: {:?}", parse_config(valid));
for raw in [
"port = 8080\n",
"host = localhost\nport = 70000\n",
"host = localhost\nport = zero\n",
] {
match parse_config(raw) {
Ok(config) => println!("unexpected success: {config:?}"),
Err(err) => {
println!("error: {err}");
if let Some(source) = err.source() {
println!(" caused by: {source}");
}
}
}
}
let missing = load("does-not-exist.conf");
println!("missing file became io error: {}", matches!(missing, Err(ConfigError::Io(_))));
}Report a problem with this exampleOpens a pre-filled issue on GitHub.