use std::error::Error;
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
pub enum ConfigError {
Io(io::Error),
Parse(ParseIntError),
Missing(String),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "failed to read config: {}", e),
ConfigError::Parse(e) => write!(f, "invalid numeric value: {}", e),
ConfigError::Missing(key) => write!(f, "missing required key: {}", key),
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConfigError::Io(e) => Some(e),
ConfigError::Parse(e) => Some(e),
ConfigError::Missing(_) => None,
}
}
}
impl From<io::Error> for ConfigError {
fn from(e: io::Error) -> Self {
ConfigError::Io(e)
}
}
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self {
ConfigError::Parse(e)
}
}
use std::fs;
use std::path::Path;
use crate::error::ConfigError;
#[derive(Debug)]
pub struct Config {
pub host: String,
pub port: u16,
}
impl Config {
pub fn load(path: &Path) -> Result<Config, ConfigError> {
let raw = fs::read_to_string(path)?; // io::Error -> ConfigError
let mut host = None;
let mut port = None;
for line in raw.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (key, value) = line
.split_once('=')
.ok_or_else(|| ConfigError::Missing(line.to_string()))?;
match key.trim() {
"host" => host = Some(value.trim().to_string()),
"port" => port = Some(value.trim().parse::<u16>()?), // ParseIntError -> ConfigError
_ => {}
}
}
Ok(Config {
host: host.ok_or_else(|| ConfigError::Missing("host".into()))?,
port: port.ok_or_else(|| ConfigError::Missing("port".into()))?,
})
}
}
mod config;
mod error;
use std::path::Path;
use std::process;
use config::Config;
fn main() {
match Config::load(Path::new("app.conf")) {
Ok(cfg) => {
println!("listening on {}:{}", cfg.host, cfg.port);
}
Err(err) => {
eprintln!("config error: {}", err);
let mut cause = std::error::Error::source(&err);
while let Some(c) = cause {
eprintln!(" caused by: {}", c);
cause = c.source();
}
process::exit(1);
}
}
}
This snippet shows the canonical way to build a domain-specific error type in Rust so that the ? operator can transparently bubble up errors from different libraries. The core idea is that ? on a Result<T, E> calls From::from on the error before returning it, so any error type that implements From<SourceError> for the function's declared error type will convert automatically. Defining one enum that gathers every failure mode a subsystem can produce, plus the right From impls, turns noisy match-and-map boilerplate into terse fallible code.
In error.rs, ConfigError is an enum with one variant per underlying failure: Io wraps std::io::Error, Parse wraps a std::num::ParseIntError, and Missing carries a plain String for a domain rule violation. Implementing Display gives human-readable messages, and implementing std::error::Error with a source method preserves the underlying cause so callers can walk the error chain. The source implementation returns Some(&dyn Error) for the wrapped variants and None for the domain-only Missing variant, which is exactly what tools like anyhow or logging layers expect.
The two From impls are what make ? ergonomic: From<io::Error> and From<ParseIntError> let the compiler insert the conversion at each ? site. Writing these by hand is the manual equivalent of what the thiserror crate generates, and understanding it clarifies why ? "just works" once the conversions exist.
In config.rs, Config::load demonstrates the payoff. fs::read_to_string(path)? can fail with an io::Error, .parse::<u16>()? can fail with a ParseIntError, and both are converted into ConfigError on the fly because the function returns Result<Config, ConfigError>. The ok_or_else call produces a ConfigError::Missing directly for the business rule that port must exist, showing that not every error needs a From impl. A key trade-off: a single flat enum couples callers to every possible variant, so very large systems sometimes split errors per module or use boxed trait objects instead. For a focused subsystem this explicit enum stays readable, gives exhaustive matching, and keeps zero-cost conversions at every ?.
Related snips
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
Share this code
Here's the card — post it anywhere.