Error Handling (Deep Dive)
TypeScript try/catch vs Rust Result<T, E>
Section titled “TypeScript try/catch vs Rust Result<T, E>”TypeScript errors are invisible at the type level — any function can throw anything at any time, and the compiler does not force you to handle it. Rust encodes fallibility directly in the return type: a function that can fail returns Result<T, E>, where T is the success value and E is the error. If you ignore the Result, the compiler warns you.
// TypeScript — errors are invisible in the signaturefunction parseId(s: string): number { const n = parseInt(s, 10); if (isNaN(n)) throw new Error(`Invalid id: ${s}`); if (n === 0) throw new Error("id zero not allowed"); return n;}
try { console.log(parseId("42")); // 42 console.log(parseId("abc")); // throws} catch (err) { console.error(err instanceof Error ? err.message : err);}use std::num::ParseIntError;
#[derive(Debug)]enum AppError { Parse(ParseIntError), NotFound(String),}
impl std::fmt::Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { AppError::Parse(e) => write!(f, "parse error: {e}"), AppError::NotFound(s) => write!(f, "not found: {s}"), } }}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::Parse(e) }}
fn parse_id(s: &str) -> Result<u32, AppError> { let n: u32 = s.trim().parse()?; // ? converts ParseIntError via From if n == 0 { return Err(AppError::NotFound(String::from("id zero not allowed"))); } Ok(n)}
fn main() { println!("{:?}", parse_id("42")); println!("{:?}", parse_id("abc")); println!("{:?}", parse_id("0"));}The ? operator
Section titled “The ? operator”The ? at the end of a Result-returning expression is syntactic sugar for “if this is Err, convert the error and return early; otherwise unwrap the Ok value.” It replaces dozens of explicit match branches.
// Without ?let n = match s.trim().parse::<u32>() { Ok(val) => val, Err(e) => return Err(AppError::from(e)),};
// With ? — identical behaviour, one characterlet n: u32 = s.trim().parse()?;For ? to work across different error types, the target error type must implement From<SourceError>. The standard library and thiserror handle this automatically.
thiserror — ergonomic custom errors
Section titled “thiserror — ergonomic custom errors”The thiserror crate eliminates the boilerplate of writing Display and From by hand. A #[derive(Error)] annotation and a #[error("...")] attribute on each variant is all you need.
// TypeScript — custom error class hierarchyclass AppError extends Error { constructor(message: string) { super(message); this.name = "AppError"; }}
class ParseError extends AppError { constructor(public input: string) { super(`Invalid input: ${input}`); }}
class NotFoundError extends AppError { constructor(public id: string) { super(`Not found: ${id}`); }}use thiserror::Error;
#[derive(Error, Debug)]enum AppError { #[error("not found: {0}")] NotFound(String),
#[error("parse error: {0}")] Parse(#[from] std::num::ParseIntError),}
fn parse_id(s: &str) -> Result<u32, AppError> { let n: u32 = s.trim().parse()?; // ParseIntError -> AppError::Parse via #[from] if n == 0 { return Err(AppError::NotFound(String::from("id zero not allowed"))); } Ok(n)}
fn main() { match parse_id("42") { Ok(id) => println!("Parsed: {id}"), Err(e) => println!("Error: {e}"), } match parse_id("abc") { Ok(id) => println!("Parsed: {id}"), Err(e) => println!("Error: {e}"), } match parse_id("0") { Ok(id) => println!("Parsed: {id}"), Err(e) => println!("Error: {e}"), }}anyhow — application-level error handling
Section titled “anyhow — application-level error handling”thiserror is for library code where callers need to inspect the error type. anyhow is for application/binary code where you just want errors to propagate cleanly with good context messages and you do not need to match on specific variants.
use anyhow::{Context, Result};
fn read_config(path: &str) -> Result<String> { std::fs::read_to_string(path) .with_context(|| format!("failed to read config at {path}"))}anyhow::Result<T> is an alias for Result<T, anyhow::Error>. Any error that implements std::error::Error can be converted into anyhow::Error with ?.
Try it
Section titled “Try it”use thiserror::Error;
#[derive(Error, Debug)]enum AppError { #[error("not found: {0}")] NotFound(String), #[error("parse error: {0}")] Parse(#[from] std::num::ParseIntError),}
fn parse_id(s: &str) -> Result<u32, AppError> { let n: u32 = s.trim().parse()?; if n == 0 { return Err(AppError::NotFound(String::from("id zero not allowed"))); } Ok(n)}
fn main() { match parse_id("42") { Ok(id) => println!("Parsed: {id}"), Err(e) => println!("Error: {e}"), } match parse_id("abc") { Ok(id) => println!("Parsed: {id}"), Err(e) => println!("Error: {e}"), } match parse_id("0") { Ok(id) => println!("Parsed: {id}"), Err(e) => println!("Error: {e}"), }}Compiling…