BasicsImpCodeStory_ErrorHandling.html
copyright © James Fawcett
Revised: 05/11/2026
7.0 Prologue
Programs encounter two broad categories of failure: recoverable errors
(file not found, invalid input, network timeout) that callers should handle, and
unrecoverable errors (null dereference, assertion violation, stack
overflow) that indicate a programming bug. Languages differ substantially in how they
represent, propagate, and distinguish these two categories.
7.1 Exceptions
Exceptions interrupt normal control flow and unwind the call stack
until a matching handler is found. They separate error signaling from error handling
at the cost of making failure paths invisible in function signatures.
// C++: throw / try / catch (no checked exceptions)
void open(const std::string& path) {
if (!exists(path)) throw std::runtime_error("not found");
}
try { open("data.csv"); }
catch (const std::exception& e) { std::cerr << e.what(); }
// C#: similar to C++; finally runs regardless of exception
try {
Open("data.csv");
} catch (FileNotFoundException e) {
Console.WriteLine(e.Message);
} finally {
Cleanup();
}
// Python: raise / try / except / finally / else
try:
open("data.csv")
except FileNotFoundError as e:
print(e)
finally:
cleanup()
Java has checked exceptions: the compiler requires callers to declare
or handle exceptions from the method signature. C++, C#, and Python use unchecked
exceptions only. Checked exceptions improve discoverability but are controversial
because they add verbosity and are often swallowed.
7.2 Result and Option Types
An alternative to exceptions is encoding failure in the return type. This makes error
paths explicit in the type signature and forces callers to acknowledge them.
// Rust: Result<T, E> for fallible operations
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
// Rust: Option<T> for values that may be absent
fn first(v: &[i32]) -> Option<i32> {
if v.is_empty() { None } else { Some(v[0]) }
}
// Pattern-matching to handle each case
match read_file("data.csv") {
Ok(contents) => process(contents),
Err(e) => eprintln!("error: {e}"),
}
C# 8+ introduced nullable reference types (string?) and
System.Nullable<T> for value types. F# and Haskell provide
Option/Maybe. Python uses None and optional
typing annotations but does not enforce them at runtime.
7.3 Panic and Abort
Panic (Rust) or assertion failure is appropriate for programming
errors - conditions that should never occur if the code is correct. It terminates
the current thread (by default, with stack unwinding) rather than propagating an error
value.
// Rust: panic! terminates the current thread
panic!("unreachable state");
// Assertion variants (all panic on failure in debug builds)
assert!(condition);
assert_eq!(left, right);
debug_assert!(condition); // compiled out in release mode
// C++: assert from <cassert> (aborts on failure)
assert(ptr != nullptr);
// C#: Debug.Assert (active only in Debug builds)
Debug.Assert(condition, "message");
// Python: assert (can be disabled with -O flag)
assert condition, "message"
Panic and abort are not for recoverable conditions. Using unwrap() or
expect() on a Result or Option in production
code is a deliberate choice to treat a failure as a bug.
7.4 Error Propagation
When a function encounters an error it cannot handle, it should propagate it to the
caller. In exception-based languages this happens automatically on throw. In
value-based error handling, propagation requires explicit code - or a shorthand.
// Rust: ? operator propagates Err/None to the caller
fn read_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(path)?; // returns Err if fails
let cfg = serde_json::from_str(&text)?; // returns Err if fails
Ok(cfg)
}
// Without ?: equivalent verbose form
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) => return Err(e.into()),
};
The ? operator also applies From::from to convert the error
type, allowing functions with a single broad error type to receive errors from multiple
sources. The thiserror and anyhow crates simplify defining
and wrapping custom error types.
7.5 Epilogue
Exceptions make the happy path clean but hide failures; value-based error handling
makes failures explicit but requires discipline to propagate. Rust’s ?
operator combines explicitness with low syntactic overhead. The next chapter examines
how programs manage the memory that holds all these values.
7.6 References
Rust Error Handling - The Book
C++ Exceptions - cppreference
C# Exceptions - Microsoft
Python Errors and Exceptions