โข 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
// 1016: Error Context
// Add context/backtrace to errors manually using wrapper structs
use std::fmt;
// Approach 1: Context wrapper struct
#[derive(Debug)]
struct ErrorWithContext {
message: String,
context: Vec<String>,
}
impl ErrorWithContext {
fn new(message: impl Into<String>) -> Self {
ErrorWithContext {
message: message.into(),
context: Vec::new(),
}
}
fn with_context(mut self, ctx: impl Into<String>) -> Self {
self.context.push(ctx.into());
self
}
}
impl fmt::Display for ErrorWithContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.context.is_empty() {
write!(f, "{}", self.message)
} else {
let chain: Vec<&str> = self.context.iter().rev().map(|s| s.as_str()).collect();
write!(f, "{}: {}", chain.join(" -> "), self.message)
}
}
}
impl std::error::Error for ErrorWithContext {}
// Extension trait for adding context to any Result
trait Context<T> {
fn context(self, ctx: impl Into<String>) -> Result<T, ErrorWithContext>;
fn with_context(self, f: impl FnOnce() -> String) -> Result<T, ErrorWithContext>;
}
impl<T> Context<T> for Result<T, ErrorWithContext> {
fn context(self, ctx: impl Into<String>) -> Result<T, ErrorWithContext> {
self.map_err(|e| e.with_context(ctx))
}
fn with_context(self, f: impl FnOnce() -> String) -> Result<T, ErrorWithContext> {
self.map_err(|e| e.with_context(f()))
}
}
// Low-level functions
fn read_file(path: &str) -> Result<String, ErrorWithContext> {
if path == "/missing" {
Err(ErrorWithContext::new("file not found"))
} else {
Ok("42".into())
}
}
fn parse_config(content: &str) -> Result<i64, ErrorWithContext> {
content
.parse::<i64>()
.map_err(|e| ErrorWithContext::new(format!("invalid number: {}", e)))
}
// Approach 2: Chain contexts through layers
fn load_setting(path: &str) -> Result<i64, ErrorWithContext> {
let content = read_file(path).context("reading config")?;
let value = parse_config(&content).context("parsing config")?;
Ok(value)
}
fn init_system(path: &str) -> Result<i64, ErrorWithContext> {
load_setting(path).context("system init")
}
fn main() {
match init_system("/missing") {
Ok(v) => println!("Value: {}", v),
Err(e) => println!("Error: {}", e),
// Prints: "system init -> reading config: file not found"
}
println!("Run `cargo test` to verify all examples.");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_context() {
let err = ErrorWithContext::new("oops");
assert_eq!(err.to_string(), "oops");
}
#[test]
fn test_single_context() {
let err = ErrorWithContext::new("oops").with_context("loading");
assert_eq!(err.to_string(), "loading: oops");
}
#[test]
fn test_nested_context() {
let result = init_system("/missing");
let err = result.unwrap_err();
assert_eq!(err.context.len(), 2);
assert!(err.to_string().contains("system init"));
assert!(err.to_string().contains("reading config"));
assert!(err.to_string().contains("file not found"));
}
#[test]
fn test_success_passes_through() {
assert_eq!(init_system("/ok").unwrap(), 42);
}
#[test]
fn test_context_trait() {
let result: Result<i64, ErrorWithContext> = Err(ErrorWithContext::new("base"));
let result = result.context("layer1");
let result = result.map_err(|e| e.with_context("layer2"));
let err = result.unwrap_err();
assert_eq!(err.context.len(), 2);
}
#[test]
fn test_lazy_context() {
let result: Result<i64, ErrorWithContext> = Err(ErrorWithContext::new("base"));
let result = result.with_context(|| format!("dynamic context {}", 42));
let err = result.unwrap_err();
assert!(err.to_string().contains("dynamic context 42"));
}
}
(* 1016: Error Context *)
(* Adding context/backtrace to errors manually *)
type error_with_context = {
message: string;
context: string list; (* stack of context strings *)
}
let make_error msg = { message = msg; context = [] }
let add_context ctx err =
{ err with context = ctx :: err.context }
let display_error err =
let chain = String.concat " -> " (List.rev err.context) in
if chain = "" then err.message
else Printf.sprintf "%s: %s" chain err.message
(* Approach 1: Manual context threading *)
let read_file path =
if path = "/missing" then Error (make_error "file not found")
else Ok "42"
let parse_config content =
match int_of_string_opt content with
| None -> Error (make_error "invalid number")
| Some n -> Ok n
let load_setting path =
match read_file path with
| Error e -> Error (add_context "reading config" e)
| Ok content ->
match parse_config content with
| Error e -> Error (add_context "parsing config" e)
| Ok n -> Ok n
let init_system path =
match load_setting path with
| Error e -> Error (add_context "system init" e)
| Ok n -> Ok n
(* Approach 2: Result pipe with context *)
let ( >>| ) r ctx =
match r with
| Ok v -> Ok v
| Error e -> Error (add_context ctx e)
let load_setting_pipe path =
(read_file path >>| "reading config")
|> Result.bind (fun content ->
parse_config content >>| "parsing config")
let test_manual () =
(match init_system "/missing" with
| Error e ->
let msg = display_error e in
assert (String.length msg > 0);
assert (List.length e.context = 2)
| Ok _ -> assert false);
assert (init_system "/ok" = Ok 42);
Printf.printf " Approach 1 (manual context): passed\n"
let test_pipe () =
(match load_setting_pipe "/missing" with
| Error e -> assert (List.length e.context = 1)
| Ok _ -> assert false);
assert (load_setting_pipe "/ok" = Ok 42);
Printf.printf " Approach 2 (pipe with context): passed\n"
let () =
Printf.printf "Testing error context:\n";
test_manual ();
test_pipe ();
Printf.printf "โ All tests passed\n"
| Aspect | OCaml | Rust | ||
|---|---|---|---|---|
| Context accumulator | `string list` field | `Vec<String>` field | ||
| Adding context | Custom `>> | ` operator | `.context()` trait method | |
| Lazy context | Thunk `fun () -> ...` | Closure `\ | \ | format!(...)` |
| Standard library | No | No (but `anyhow` is de facto standard) | ||
| Display format | Manual `String.concat` | Custom `Display` impl |