Code Story

Chapter #7 – Error Handling

exceptions, Result/Option, panic, error propagation

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