Exceptions vs. result types: two philosophies of error handling
Some languages throw errors up the stack; others return them as values. Each approach makes different mistakes easy or hard.
Every program has to deal with things going wrong. Languages broadly choose between two approaches, and the choice shapes how code reads.
Exceptions
Python, Ruby, Java, C# and JavaScript throw exceptions that unwind the stack until something catches them.
try:
config = load_config("app.toml")
except FileNotFoundError:
config = default_config()
- Pro: the happy path stays clean; errors propagate automatically.
- Con: a function's signature rarely tells you what it can throw, so it's easy to forget a case — or catch too broadly.
Errors as values
Go returns errors alongside results:
cfg, err := loadConfig("app.toml")
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
Rust encodes the possibility of failure in the type:
fn load_config(path: &str) -> Result<Config, ConfigError> {
let text = std::fs::read_to_string(path)?; // ? returns early on error
let cfg = toml::from_str(&text)?;
Ok(cfg)
}
- Pro: failure is visible in the signature; the compiler (in Rust) won't let you use the value without handling the error.
- Con: more ceremony, although operators like
?cut most of it.
Good practice either way
- Add context. "file not found" is useless; "loading config from app.toml: file not found" is actionable.
- Handle errors where you can do something about them. Otherwise propagate.
- Don't swallow errors. An empty
catchor an ignorederrhides bugs. - Separate expected failures from bugs. Invalid user input is expected — return a validation error. A violated invariant is a bug — crash loudly.
Which is better?
Neither universally. Exceptions suit applications where most errors bubble up to a single top-level handler. Result types suit libraries and systems code where every caller needs to know exactly what can fail. Many languages now borrow from both.


