Result<T, E
Result<T, E>
Level 2 — Control Flow & Data Structures An enum (
Ok(T)/Err(E)) for recoverable error handling.
1. Prerequisites
- Enum —
Resultis a standard Enum built into the Rust standard library. Option<T>— The sister type toResult, used for missing data rather than failed operations.match— The primary tool used to check if aResultsucceeded or failed.
2. Term Category
Rust-specific (the safety): Rust completely removes the concept of try / catch blocks and Exceptions. Instead, any function that can fail simply returns the Result<T, E> enum, forcing the developer to handle errors predictably and explicitly.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In older languages like Java, C#, or Python, errors are handled by "throwing Exceptions". This design has two major flaws:
- Invisibility: By looking at a function signature like
int divide(a, b), you have no idea if it might throw an Exception and crash. - Forgetfulness: If you forget to wrap a risky function in a
try/catchblock, your program will crash at runtime.
Rust's designers wanted errors to be visible, predictable, and impossible to ignore. Therefore, Rust uses an Enum called Result. If a function might fail, it is forced to return a Result type. It has two variants:
Ok(value)— The operation succeeded, here is your data.Err(error_info)— The operation failed, here is why.
Because Result is an Enum, the compiler forces you to handle both the Ok case and the Err case (usually via match). You cannot accidentally ignore a failure, making Rust programs incredibly stable.
(2) Reality Metaphor
Imagine ordering a package online.
In a language with Exceptions, the delivery driver either hands you your package, or they secretly plant a landmine on your porch. If you open your front door without wearing a bomb squad suit (try/catch), the landmine explodes and you die (program crash).
In Rust, the delivery driver always hands you a transparent lockbox (the Result enum). You look inside the lockbox: it either contains your item (Ok), or it contains an apology note explaining why the delivery failed (Err). You cannot touch the item without first opening the lockbox and acknowledging the note.
(3) Rust Code Examples
Short Snippet (The Definition)
You don't need to define Result yourself; it's built into the language. It looks like this:
enum Result<T, E> {
Ok(T),
Err(E),
}
Because it's so common, Rust automatically imports Ok and Err for you.
let success: Result<i32, String> = Ok(200);
let failure: Result<i32, String> = Err(String::from("Database offline"));
Fuller Example (Handling the Result)
// A function that can fail returns a `Result`
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0.0 {
// Return the Error variant
Err(String::from("Cannot divide by zero!"))
} else {
// Return the Success variant
Ok(numerator / denominator)
}
}
fn main() {
let outcome = divide(10.0, 0.0);
// We MUST use pattern matching to extract the answer
match outcome {
Ok(answer) => println!("The answer is: {}", answer),
Err(error_msg) => println!("Task failed: {}", error_msg),
}
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Result T E Scoping and Lifecycle Rules
The mistake: Assuming Result T E instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("result_t_e_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("result_t_e_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Result T E State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Result T E through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Result T E Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Result T E instances across OS threads via std::thread::spawn.
Why it's wrong: Traits that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Financial Payment Gateway Transaction Parser & Batch Evaluator
Scenario: A fintech payment gateway receives raw transaction records from external banking webhooks in CSV format ("TX_ID,AMOUNT_CENTS,CURRENCY"). Because external input is untrusted, parsing can fail at multiple stages: invalid field count, malformed transaction prefix (must start with "TX_"), invalid or non-positive integer amounts, or unsupported currency codes (only "USD", "EUR", and "GBP" are supported).
Task:
- Define a domain error type
PaymentErrorwith variants:InvalidFormatInvalidTxIdInvalidAmount(String)UnsupportedCurrency(String)
- Define a struct
PaymentTransactionwith fieldstx_id: String,amount_cents: u64, andcurrency: String. - Implement
parse_payment(raw: &str) -> Result<PaymentTransaction, PaymentError>using pattern matching andResultmapping combinators likemap_err. - Implement
process_batch(records: &[&str]) -> (Vec<PaymentTransaction>, Vec<PaymentError>)that partitions a list of raw string records into valid transactions and encountered errors.
Answer
Implementation
#[derive(Debug, PartialEq, Eq)]
pub enum PaymentError {
InvalidFormat,
InvalidTxId,
InvalidAmount(String),
UnsupportedCurrency(String),
}
#[derive(Debug, PartialEq, Eq)]
pub struct PaymentTransaction {
pub tx_id: String,
pub amount_cents: u64,
pub currency: String,
}
pub fn parse_payment(raw: &str) -> Result<PaymentTransaction, PaymentError> {
let parts: Vec<&str> = raw.split(',').map(|s| s.trim()).collect();
if parts.len() != 3 {
return Err(PaymentError::InvalidFormat);
}
let tx_id = parts[0];
if !tx_id.starts_with("TX_") || tx_id.len() <= 3 {
return Err(PaymentError::InvalidTxId);
}
let amount_cents: u64 = parts[1]
.parse::<u64>()
.map_err(|_| PaymentError::InvalidAmount(parts[1].to_string()))?;
if amount_cents == 0 {
return Err(PaymentError::InvalidAmount(
"Amount must be greater than zero".to_string(),
));
}
let currency = parts[2];
match currency {
"USD" | "EUR" | "GBP" => {}
_ => return Err(PaymentError::UnsupportedCurrency(currency.to_string())),
}
Ok(PaymentTransaction {
tx_id: tx_id.to_string(),
amount_cents,
currency: currency.to_string(),
})
}
pub fn process_batch(records: &[&str]) -> (Vec<PaymentTransaction>, Vec<PaymentError>) {
let mut successes = Vec::new();
let mut failures = Vec::new();
for record in records {
match parse_payment(record) {
Ok(tx) => successes.push(tx),
Err(err) => failures.push(err),
}
}
(successes, failures)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_payment_parsing() {
let raw = "TX_9901, 15000, USD";
let res = parse_payment(raw);
assert!(res.is_ok());
let tx = res.unwrap();
assert_eq!(tx.tx_id, "TX_9901");
assert_eq!(tx.amount_cents, 15000);
assert_eq!(tx.currency, "USD");
}
#[test]
fn test_invalid_format_and_id() {
let bad_format = "TX_9901,15000";
assert_eq!(parse_payment(bad_format), Err(PaymentError::InvalidFormat));
let bad_id = "INVALID_9901,15000,USD";
assert_eq!(parse_payment(bad_id), Err(PaymentError::InvalidTxId));
}
#[test]
fn test_amount_and_currency_errors() {
let parse_err = parse_payment("TX_10, -500, USD");
assert!(matches!(parse_err, Err(PaymentError::InvalidAmount(_))));
let zero_err = parse_payment("TX_10, 0, USD");
assert_eq!(
zero_err,
Err(PaymentError::InvalidAmount("Amount must be greater than zero".to_string()))
);
let curr_err = parse_payment("TX_10, 500, JPY");
assert_eq!(curr_err, Err(PaymentError::UnsupportedCurrency("JPY".to_string())));
assert_ne!(curr_err, Err(PaymentError::UnsupportedCurrency("USD".to_string())));
}
#[test]
fn test_process_batch() {
let records = vec![
"TX_1, 100, USD",
"INVALID",
"TX_2, 200, EUR",
"TX_3, 0, GBP",
];
let (successes, failures) = process_batch(&records);
assert_eq!(successes.len(), 2);
assert_eq!(failures.len(), 2);
assert_eq!(successes[0].tx_id, "TX_1");
assert_eq!(failures[0], PaymentError::InvalidFormat);
assert!(matches!(failures[1], PaymentError::InvalidAmount(_)));
}
}
Technical Explanation
- Structured Error Hierarchy: Returning a domain-specific
PaymentErrorenum instead of a genericStringorpanic!allows callers to programmatically inspect failure reasons. Rust requires all enum variants to be exhaustively handled during pattern matching. - Error Translation with
map_err: Thestr::parse::<u64>()method returns aResult<u64, ParseIntError>. We map standard library errors into domain errors using.map_err(|_| PaymentError::InvalidAmount(...)). - Ownership and Lifetime Boundaries:
parse_paymentaccepts a borrowed slice&str(raw). When creating the ownedPaymentTransaction, string fields are converted into ownedStringobjects via.to_string(), ensuring the output struct outlives the input string slice. - Batch Processing Pattern:
process_batchdemonstrates howResultinstances can be collected or split without aborting execution on the first error encountered, a standard requirement for robust stream parsing pipelines.
Exercise 2: Binary Frame Packet Decoder with Checksum Validation & Chained Combinators
Scenario: A low-level telemetry ingestion worker receives binary byte streams over network sockets. Packets follow a fixed header structure:
- Bytes 0..2: Magic byte header
0xA5,0x5A. - Bytes 2..4: Big-endian
u16specifying payload byte length . - Bytes 4..(4+N): Raw payload bytes.
- Byte (4+N): XOR checksum byte of all bytes preceding the checksum (header + length + payload).
Task:
- Define a
PacketErrorenum covering failure modes:BufferTooShort { required: usize, actual: usize }InvalidMagicHeader([u8; 2])ChecksumMismatch { expected: u8, calculated: u8 }PayloadExceedsLimit(usize)InvalidPayloadUtf8(String)
- Implement
decode_packet(bytes: &[u8]) -> Result<Packet, PacketError>. - Implement
decode_and_extract_text(bytes: &[u8]) -> Result<String, PacketError>using.and_then()to chain binary packet decoding with UTF-8 string extraction.
Answer
Implementation
#[derive(Debug, PartialEq, Eq)]
pub enum PacketError {
BufferTooShort { required: usize, actual: usize },
InvalidMagicHeader([u8; 2]),
ChecksumMismatch { expected: u8, calculated: u8 },
PayloadExceedsLimit(usize),
InvalidPayloadUtf8(String),
}
#[derive(Debug, PartialEq, Eq)]
pub struct Packet {
pub payload: Vec<u8>,
}
pub fn decode_packet(bytes: &[u8]) -> Result<Packet, PacketError> {
const MIN_HEADER_LEN: usize = 5; // 2 magic + 2 len + 1 checksum
if bytes.len() < MIN_HEADER_LEN {
return Err(PacketError::BufferTooShort {
required: MIN_HEADER_LEN,
actual: bytes.len(),
});
}
let magic = [bytes[0], bytes[1]];
if magic != [0xA5, 0x5A] {
return Err(PacketError::InvalidMagicHeader(magic));
}
let payload_len = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
if payload_len > 1024 {
return Err(PacketError::PayloadExceedsLimit(payload_len));
}
let total_required = 4 + payload_len + 1;
if bytes.len() < total_required {
return Err(PacketError::BufferTooShort {
required: total_required,
actual: bytes.len(),
});
}
let data_to_checksum = &bytes[..4 + payload_len];
let calculated_checksum = data_to_checksum.iter().fold(0u8, |acc, &b| acc ^ b);
let expected_checksum = bytes[4 + payload_len];
if calculated_checksum != expected_checksum {
return Err(PacketError::ChecksumMismatch {
expected: expected_checksum,
calculated: calculated_checksum,
});
}
let payload = bytes[4..4 + payload_len].to_vec();
Ok(Packet { payload })
}
pub fn decode_and_extract_text(bytes: &[u8]) -> Result<String, PacketError> {
decode_packet(bytes).and_then(|pkt| {
String::from_utf8(pkt.payload)
.map_err(|err| PacketError::InvalidPayloadUtf8(err.to_string()))
})
}
#[cfg(test)]
mod tests {
use super::*;
fn build_raw_packet(payload: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0xA5);
buf.push(0x5A);
let len_bytes = (payload.len() as u16).to_be_bytes();
buf.extend_from_slice(&len_bytes);
buf.extend_from_slice(payload);
let checksum = buf.iter().fold(0u8, |acc, &b| acc ^ b);
buf.push(checksum);
buf
}
#[test]
fn test_valid_packet_decoding() {
let raw = build_raw_packet(b"hello rust");
let res = decode_packet(&raw);
assert!(res.is_ok());
let pkt = res.unwrap();
assert_eq!(pkt.payload, b"hello rust");
}
#[test]
fn test_header_and_short_buffer() {
let short = vec![0xA5, 0x5A];
assert_eq!(
decode_packet(&short),
Err(PacketError::BufferTooShort { required: 5, actual: 2 })
);
let bad_magic = vec![0x11, 0x22, 0x00, 0x00, 0x33];
assert_eq!(
decode_packet(&bad_magic),
Err(PacketError::InvalidMagicHeader([0x11, 0x22]))
);
}
#[test]
fn test_checksum_mismatch() {
let mut raw = build_raw_packet(b"test");
let last_idx = raw.len() - 1;
raw[last_idx] ^= 0xFF;
let res = decode_packet(&raw);
assert!(matches!(res, Err(PacketError::ChecksumMismatch { .. })));
if let Err(PacketError::ChecksumMismatch { expected, calculated }) = res {
assert_ne!(expected, calculated);
} else {
panic!("Expected ChecksumMismatch error variant");
}
}
#[test]
fn test_decode_and_extract_text_combinator() {
let valid_raw = build_raw_packet("Level 2 Result".as_bytes());
let text_res = decode_and_extract_text(&valid_raw);
assert_eq!(text_res, Ok("Level 2 Result".to_string()));
let invalid_utf8_bytes = vec![0xFF, 0xFE, 0xFD];
let invalid_utf8_raw = build_raw_packet(&invalid_utf8_bytes);
let text_err = decode_and_extract_text(&invalid_utf8_raw);
assert!(matches!(text_err, Err(PacketError::InvalidPayloadUtf8(_))));
}
}
Technical Explanation
- Defensive Slice Bounds Checking: Slicing byte arrays
&bytes[..]panics at runtime if indices are out of bounds. Checkingbytes.len() < MIN_HEADER_LENandbytes.len() < total_requiredexplicitly converts potential panics into cleanResult::Errvalues (PacketError::BufferTooShort). - Chaining Operations with
.and_then(): Theand_thencombinator takes a closure returningResult<U, E>and applies it only if the precedingResultisOk. This flattens what would otherwise be nestedmatchstatements into a single monadic sequence. - Conversion of Foreign Errors:
String::from_utf8returnsFromUtf8Error. By calling.map_err(|err| PacketError::InvalidPayloadUtf8(err.to_string())), foreign error types are unified into the module's target error enum. - Endianness Safety: Converting raw multi-byte arrays to numerical fields via
u16::from_be_bytesensures architecture-independent binary protocol parsing across big-endian and little-endian systems.
Exercise 3: Multi-Tier Service Configuration Evaluator with Fallback Recovery
Scenario: A cloud container bootstrap process configures service instances from environment parameters. Configuration strings contain key-value assignments ("KEY=VALUE"). The engine must parse mandatory settings (NODE_ID, TIMEOUT_MS, MAX_CONN) and enforce range bounds. If the primary configuration string fails validation (e.g., due to bad formatting or invalid values), the runtime must seamlessly fall back to a default configuration string using Result fallback combinators.
Task:
- Define a struct
Configwith fieldsnode_id: u32,timeout_ms: u64, andmax_connections: u32. - Define
ConfigErrorenum with variants:MissingField(String)ParseIntError { field: String, raw_value: String }OutOfBounds { field: String, value: u64, min: u64, max: u64 }
- Implement
parse_config(raw: &str) -> Result<Config, ConfigError>. - Implement
load_config_with_fallback(primary_env: &str, secondary_env: &str) -> Result<(Config, bool), ConfigError>using.or_else()to tryprimary_envfirst and fall back tosecondary_envifprimary_envyields anErr.
Answer
Implementation
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Config {
pub node_id: u32,
pub timeout_ms: u64,
pub max_connections: u32,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ConfigError {
MissingField(String),
ParseIntError { field: String, raw_value: String },
OutOfBounds { field: String, value: u64, min: u64, max: u64 },
}
pub fn parse_config(raw: &str) -> Result<Config, ConfigError> {
let mut map = HashMap::new();
for line in raw.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, val)) = line.split_once('=') {
map.insert(key.trim(), val.trim());
}
}
let get_field = |name: &str| -> Result<&str, ConfigError> {
map.get(name)
.copied()
.ok_or_else(|| ConfigError::MissingField(name.to_string()))
};
let raw_node_id = get_field("NODE_ID")?;
let node_id: u32 = raw_node_id.parse().map_err(|_| ConfigError::ParseIntError {
field: "NODE_ID".to_string(),
raw_value: raw_node_id.to_string(),
})?;
if node_id < 1 || node_id > 65535 {
return Err(ConfigError::OutOfBounds {
field: "NODE_ID".to_string(),
value: node_id as u64,
min: 1,
max: 65535,
});
}
let raw_timeout = get_field("TIMEOUT_MS")?;
let timeout_ms: u64 = raw_timeout.parse().map_err(|_| ConfigError::ParseIntError {
field: "TIMEOUT_MS".to_string(),
raw_value: raw_timeout.to_string(),
})?;
if timeout_ms < 100 || timeout_ms > 60000 {
return Err(ConfigError::OutOfBounds {
field: "TIMEOUT_MS".to_string(),
value: timeout_ms,
min: 100,
max: 60000,
});
}
let raw_max_conn = get_field("MAX_CONN")?;
let max_connections: u32 = raw_max_conn.parse().map_err(|_| ConfigError::ParseIntError {
field: "MAX_CONN".to_string(),
raw_value: raw_max_conn.to_string(),
})?;
if max_connections < 1 || max_connections > 10000 {
return Err(ConfigError::OutOfBounds {
field: "MAX_CONN".to_string(),
value: max_connections as u64,
min: 1,
max: 10000,
});
}
Ok(Config {
node_id,
timeout_ms,
max_connections,
})
}
pub fn load_config_with_fallback(
primary_env: &str,
secondary_env: &str,
) -> Result<(Config, bool), ConfigError> {
parse_config(primary_env)
.map(|cfg| (cfg, false))
.or_else(|_| parse_config(secondary_env).map(|cfg| (cfg, true)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_config_parsing() {
let raw = "NODE_ID=101\nTIMEOUT_MS=5000\nMAX_CONN=250";
let res = parse_config(raw);
assert!(res.is_ok());
let cfg = res.unwrap();
assert_eq!(cfg.node_id, 101);
assert_eq!(cfg.timeout_ms, 5000);
assert_eq!(cfg.max_connections, 250);
}
#[test]
fn test_missing_field_and_out_of_bounds() {
let missing = "NODE_ID=101\nTIMEOUT_MS=5000";
assert_eq!(
parse_config(missing),
Err(ConfigError::MissingField("MAX_CONN".to_string()))
);
let oob = "NODE_ID=101\nTIMEOUT_MS=50\nMAX_CONN=250";
assert_eq!(
parse_config(oob),
Err(ConfigError::OutOfBounds {
field: "TIMEOUT_MS".to_string(),
value: 50,
min: 100,
max: 60000,
})
);
}
#[test]
fn test_fallback_chain() {
let invalid_primary = "NODE_ID=invalid\nTIMEOUT_MS=5000\nMAX_CONN=250";
let valid_secondary = "NODE_ID=202\nTIMEOUT_MS=1000\nMAX_CONN=500";
let res = load_config_with_fallback(invalid_primary, valid_secondary);
assert!(res.is_ok());
let (cfg, used_fallback) = res.unwrap();
assert!(used_fallback);
assert_eq!(cfg.node_id, 202);
assert_ne!(cfg.node_id, 101);
let both_invalid = load_config_with_fallback("BAD=1", "ALSO_BAD=2");
assert!(matches!(both_invalid, Err(ConfigError::MissingField(_))));
}
}
Technical Explanation
- Lazy Fallback Evaluation with
.or_else(): The.or_else()combinator takes a closure that evaluates only when the initialResultis anErr. This avoids unnecessary computation or secondary I/O if the primary operation succeeds. - Option-to-Result Conversion via
.ok_or_else(): When retrieving fields fromHashMap::get, anOption<&str>is returned. ConvertingOptiontoResultusing.ok_or_else(|| ConfigError::MissingField(...))creates a lazy error payload if the value isNone. - The
?Operator Mechanics: The?operator unwrapsOk(T)values or early-returnsErr(E)values from the current function. This allows sequential validation logic to remain clean while propagating errors instantly. - Boundary Validation: Numerical parsing is complemented by semantic range assertions. Even if string conversion to
u32succeeds, invalid operational ranges (e.g. timeout < 100ms) yield structured domain errors (OutOfBounds).
6. Related Terms
Option<T>— The sister enum used for missing data, whereas Result is used for failed operations.?Operator — (Future reference) The magical syntax sugar that makes working withResultincredibly easy by automatically returning errors up the chain.unwrap()/expect()— Aggressive methods that intentionally crash the program if aResultis anErr.- Enum — Related concept: Enum.
FromIterator/ExtendTraits — Related concept:FromIterator/ExtendTraits.FromStrTrait &.parse()— Related concept:FromStrTrait &.parse().- Type Alias — Related concept: Type Alias.
TryFrom/TryInto— Related concept:TryFrom/TryInto.
7. Key Takeaways
- Rust does not use Exceptions (
try/catch). It uses theResult<T, E>enum. - The variants are
Ok(T)(success with data) andErr(E)(failure with error data). - The compiler forces you to handle the
Result(usually withmatchorif let) to extract theOkvalue. - This design forces you to acknowledge and handle errors gracefully, preventing unexpected runtime crashes.