โข Option
โข Result
โข The ? operator propagates errors up the call stack concisely
โข Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations
โข The compiler forces you to handle every error case โ no silent failures
โข Option
โข Result
โข The ? operator propagates errors up the call stack concisely
โข Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations
โข The compiler forces you to handle every error case โ no silent failures
// 1005: Error Chaining
// Chain errors with context using map_err
use std::fmt;
#[derive(Debug)]
enum IoError {
NotFound,
Corrupted(String),
}
impl fmt::Display for IoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IoError::NotFound => write!(f, "not found"),
IoError::Corrupted(s) => write!(f, "corrupted: {}", s),
}
}
}
#[derive(Debug)]
struct AppError {
context: String,
source: IoError,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.context, self.source)
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None // IoError doesn't impl Error in this example for simplicity
}
}
// Low-level function returning raw error
fn read_file(path: &str) -> Result<String, IoError> {
match path {
"/missing" => Err(IoError::NotFound),
"/bad" => Err(IoError::Corrupted("invalid utf-8".into())),
_ => Ok("data".into()),
}
}
// Approach 1: map_err to add context
fn load_config(path: &str) -> Result<String, AppError> {
read_file(path).map_err(|e| AppError {
context: format!("loading {}", path),
source: e,
})
}
// Approach 2: Generic context extension trait
trait WithContext<T> {
fn with_context(self, ctx: impl FnOnce() -> String) -> Result<T, AppError>;
}
impl<T> WithContext<T> for Result<T, IoError> {
fn with_context(self, ctx: impl FnOnce() -> String) -> Result<T, AppError> {
self.map_err(|e| AppError {
context: ctx(),
source: e,
})
}
}
fn load_config_ext(path: &str) -> Result<String, AppError> {
read_file(path).with_context(|| format!("loading {}", path))
}
fn main() {
match load_config("/missing") {
Ok(v) => println!("Got: {}", v),
Err(e) => println!("Error: {}", e),
}
println!("Run `cargo test` to verify all examples.");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_success() {
assert_eq!(load_config("/ok").unwrap(), "data");
}
#[test]
fn test_map_err_context() {
let err = load_config("/missing").unwrap_err();
assert_eq!(err.context, "loading /missing");
assert!(matches!(err.source, IoError::NotFound));
}
#[test]
fn test_corrupted_context() {
let err = load_config("/bad").unwrap_err();
assert!(err.to_string().contains("corrupted"));
assert!(err.to_string().contains("loading /bad"));
}
#[test]
fn test_extension_trait() {
assert_eq!(load_config_ext("/ok").unwrap(), "data");
let err = load_config_ext("/missing").unwrap_err();
assert_eq!(err.context, "loading /missing");
}
#[test]
fn test_display_format() {
let err = load_config("/missing").unwrap_err();
assert_eq!(err.to_string(), "loading /missing: not found");
}
}
(* 1005: Error Chaining *)
(* Adding context to errors as they propagate *)
type inner_error = NotFound | Corrupted of string
type app_error = { context: string; cause: inner_error }
let string_of_inner = function
| NotFound -> "not found"
| Corrupted s -> Printf.sprintf "corrupted: %s" s
let string_of_app_error e =
Printf.sprintf "%s: %s" e.context (string_of_inner e.cause)
(* Approach 1: Manual context wrapping *)
let read_file path =
if path = "/missing" then Error NotFound
else if path = "/bad" then Error (Corrupted "invalid utf-8")
else Ok "data"
let load_with_context path =
match read_file path with
| Error e -> Error { context = Printf.sprintf "loading %s" path; cause = e }
| Ok v -> Ok v
(* Approach 2: Generic context helper (like Rust's map_err) *)
let with_context ctx result =
match result with
| Error e -> Error { context = ctx; cause = e }
| Ok v -> Ok v
let load_with_helper path =
read_file path |> with_context (Printf.sprintf "loading %s" path)
let test_manual () =
(match load_with_context "/missing" with
| Error e ->
assert (e.context = "loading /missing");
assert (e.cause = NotFound)
| Ok _ -> assert false);
assert (load_with_context "/ok" = Ok "data");
Printf.printf " Approach 1 (manual context): passed\n"
let test_helper () =
(match load_with_helper "/bad" with
| Error e ->
assert (String.length (string_of_app_error e) > 0);
assert (e.cause = Corrupted "invalid utf-8")
| Ok _ -> assert false);
assert (load_with_helper "/ok" = Ok "data");
Printf.printf " Approach 2 (context helper): passed\n"
let () =
Printf.printf "Testing error chaining:\n";
test_manual ();
test_helper ();
Printf.printf "โ All tests passed\n"
| Aspect | OCaml | Rust |
|---|---|---|
| Context addition | Manual record wrapping | `map_err` / extension trait |
| Inline ergonomics | Verbose match | Fluent `.map_err(...)` |
| Error chain | Custom nesting | `Error::source()` standard |
| Ecosystem support | Ad-hoc | `anyhow`, `thiserror` crates |
| Lazy context | Closure in helper | `FnOnce` in extension trait |