Summary
Language support to improve Result
-based error handling.
Motivation
Error handling is painful in most languages:
- integer error codes are simple, but offer little safety. The possible set of values must be documented separately (and kept in sync with the code!) for each function. Invalid usage is hard to detect, forcing people to update their code on API change is even harder.
- exceptions can be nice, but the implementations of other languages are lacking in key areas (anything can throw at any time, “invisible magic goto”).
-
Result
is awesome but verbose, especially with multiple error types.
A Rusty error handling scheme would need to
- represent all possible error cases as part of the function signature.
- ensure the programmer can keep track of control flow easily. Code that throws should look like code that throws.
- handling errors at the appropriate level should be the easiest option.
- make it easy to opt out
- ideally, no overhead compared to proper error handling in any other language.
Alternatives
- Do nothing.
- Standardize on
Result<T, Errno>
, with a global repository of error codes and a reserved set of values for private use. (2^32 errors should be enough for everyone!) The drawback of course is the need to coordinate error codes. This is the most familiar option for C folks. - Variadic generics:
Result<T, Err<E1, ...>>
,Result<T, Err1, ...>
. This should be the ideal solution, but for the common case of error handling, it might not improve usablity as much as dedicated language features.
Drawbacks
- More language.
- Assumes
libcore
. - This is essentially a “poor man’s variadics”, and may become obsolete.
Detailed design
throws
fn foo() -> T throws Foo, Bar { ... }
is desugared into something like
enum Magic {
MagicFoo(Foo),
MagicBar(Bar)
}
fn foo() -> Result<T,Magic> { ... }
The generated enum and its variants are anonymous and cannot be directly referenced from code. The compiler is free to choose the representation, for example to collect all errors in a crate into a single enum, or even across crates.
throw
The throw
statement generates errors:
throw *expr*;
desugars into something like
return Err(MagicFoo(*expr*));
try
The try!
macro is promoted to an expression:
let foo = try *expr*;
desugars into something like
let foo = match *expr* {
Ok(k) => k,
Err(MagicFoo(e)) => throw e,
...
};
Again, the expression’s error list must be a subset of the surrounding function’s error list.
catch
The catch
expression deconstructs errors:
catch *expr* {
Ok(k) => ...,
e: Foo => ...,
}
desugars into something like
match *expr* {
Ok(k) => ...,
Err(MagicFoo(e)) => ...,
...
}
Unresolved questions
- the syntax and semantics of
catch
. For example,catch (try foo())
can be surprising. - whether variadic generics (or anything else) would be better suited for error handling
- nothing is set in stone, really
Code example
Stolen from http://rustbyexample.com/result.html:
mod checked {
struct DivisionByZero
struct NegativeLogarithm;
struct NegativeSquareRoot(f64);
pub fn div(x: f64, y: f64) -> f64 throws DivisionByZero {
if y == 0.0 {
throw DivisionByZero;
} else {
Ok(x / y)
}
}
pub fn sqrt(x: f64) -> f64 throws NegativeSquareRoot {
if x < 0.0 {
throw NegativeSquareRoot(x);
} else {
Ok(x.sqrt())
}
}
pub fn ln(x: f64) -> f64 throws NegativeLogarithm {
if x < 0.0 {
throw NegativeLogarithm;
} else {
Ok(x.ln())
}
}
}
// `op(x, y)` === `sqrt(ln(x / y))`
fn op(x: f64, y: f64) -> f64 {
// This was a match pyramid
let compute = |x,y| {
use checked::{div,ln,sqrt};
try sqrt(try ln(try div(x, y)))
}
catch compute(x, y) {
Ok(result) => result,
checked::DivisionByZero => fail!(),
checked::NegativeLogarithm => fail!(),
e: checked::NegativeSquareRoot => fail!("Can't compute the square root of {}", e.0),
}
}