Expected errors
In C++, throw
both produces an error (the thrown exception) and initiates
non-local control flow (unwinding to the nearest catch
block). In Rust, error
values (Option::None
or Result::Err
) are returned as normal values from a
function. Rust's return
statement can be used to return early from a function.
#include <stdexcept>
double divide(double dividend, double divisor) {
if (divisor == 0.0) {
throw std::domain_error("zero divisor");
}
return dividend / divisor;
}
#![allow(unused)] fn main() { fn divide( dividend: f64, divisor: f64, ) -> Option<f64> { if divisor == 0.0 { return None; } Some(dividend / divisor) } }
The requirement to have the return type indicate that an error is possible means
that callbacks that are permitted to have errors need to be given an Option
or
Result
return type. Omitting that is like requiring callbacks to be noexcept
in C++. Functions that do not need to indicate errors but that will be used as
callbacks where errors are permitted will need to wrap their results in
Option::Some
or Result::Ok
.
#include <stdexcept>
int produce_42() {
return 42;
}
int fail() {
throw std::runtime_error("oops");
}
int useCallback(int (*func)(void)) {
return func();
}
int main() {
try {
int x = useCallback(produce_42);
int y = useCallback(fail);
// use x and y
} catch (std::runtime_error &e) {
// handle error
}
}
fn produce_42() -> i32 { 42 } fn fail() -> Option<i32> { None } fn use_callback( f: impl Fn() -> Option<i32>, ) -> Option<i32> { f() } fn main() { // need to wrap produce_42 to match the // expected type let Some(x) = use_callback(|| Some(produce_42())) else { // handle error return; }; let Some(y) = use_callback(fail) else { // handle error return; }; // use x and y }
Handling errors
In C++, the only way to handle exceptions is catch
. In Rust, all of the
features for dealing with tagged
unions can be used with Result
and
Option
. The most approach depends on the intention of the program.
The basic way of handling an error indicated by a Result
in Rust is by using
match
.
Using match
is the most general approach, because it enables handling
additional cases explicitly and can be used as an expression. match
connotes
equal importance of all branches.
#include <vector>
#include <stdexcept>
int main() {
std::vector<int> v;
// ... populate v ...
try {
auto x = v.at(0);
// use x
} catch (std::out_of_range &e) {
// handle error
}
}
fn main() { let mut v = Vec::<i32>::new(); // ... populate v ... match v.get(0) { Some(x) => { // use x } None => { // handle error } } }
Because handling only a single variant of a Rust enum is so common, the if let
syntax support that use case. The syntax both makes it clear that only the one
case is important and reduces the levels of indentation.
if let
is less general than match
. It can also be used as an expression, but
can only distinguish one case from the rest. if let
connotes that the else
case is not the normal case, but that some default handling will occur or some
default value will be produced.
Note that with Result
, if let
does not enable accessing the error value.
fn main() { let mut v = Vec::<i32>::new(); // ... populate v ... if let Some(x) = v.get(0) { // use x } else { // handle error } }
When the error handling involves some kind of control flow operation, like
break
or return
, the let else
syntax is even more concise.
Much like normal let
statements, let else
statements can only be used where
statements are expected. let else
statements also connote that the else case
is not the normal case, and that no further (normal) processing will occur.
fn main() { let mut v = Vec::<i32>::new(); // ... populate v ... let Some(x) = v.get(0) else { // handle error return; }; // use x }
Result
and Option
also have some helper methods for handling errors.
These methods resemble the methods on std::expected
in C++.
#include <expected>
#include <string>
int main() {
std::expected<int, std::string> res(42);
auto x(res.transform([](int n) { return n * 2; }));
}
fn main() { let res: Result<i32, String> = Ok(42); let x = res.map(|n| n * 2); }
These helper methods and others are described in detail in the documentation for
Option
and
Result
.
Borrowed results
In the above examples, the successful results are borrowed from the vector. It
common to need to clone or copy the result into an owned copy, and to want to do
so without having to match on and reconstruct the value. Result
and Option
have helper methods for these purposes.
fn main() { let mut v = Vec::<i32>::new(); v.push(42); let x: Option<&i32> = v.get(0); let y: Option<i32> = v.get(0).copied(); let mut w = Vec::<String>::new(); w.push("hello".to_string()); let s: Option<&String> = w.get(0); let r: Option<String> = w.get(0).cloned(); }
Propagating errors
In C++, exceptions propagate automatically. In Rust, errors indicated by
Result
or Option
must be explicitly propagated. The ?
operator is a
convenience for this. There are also several methods for manipulating Result
and Option
that have a similar effect to propagating the error.
#include <cstddef>
#include <vector>
int accessValue(std::vector<std::size_t> indices,
std::vector<int> values,
std::size_t i) {
// vector::at throws
size_t idx(indices.at(i));
// vector::at throws
return values.at(idx);
}
#![allow(unused)] fn main() { fn access_value( indices: Vec<usize>, values: Vec<i32>, i: usize, ) -> Option<i32> { // * dereferences the &i32 to copy it // ? propagates the None let idx = *indices.get(i)?; // returns the Option directly values.get(idx).copied() } }
The above Rust example is equivalent to the following, which does not use the
?
operator. The version using ?
is more idiomatic.
#![allow(unused)] fn main() { fn access_value( indices: Vec<usize>, values: Vec<i32>, i: usize, ) -> Option<i32> { // matching through the & makes a copy of the i32 let Some(&idx) = indices.get(i) else { return None; }; // still returns the Option directly values.get(idx).copied() } }
The following example is also equivalent. It is not idiomatic (using ?
here is
more readable), but does demonstrate one of the helper methods.
Option::and_then
is similar to std::optional::and_then
in
C++23.
#![allow(unused)] fn main() { fn access_value( indices: Vec<usize>, values: Vec<i32>, i: usize, ) -> Option<i32> { // matching through the & makes a copy of the i32 indices .get(i) .and_then(|idx| values.get(*idx)) .copied() } }
These helper methods and others are described in detail in the documentation for
Option
and
Result
.
Uncaught exceptions in main
In C++ when an exception is uncaught, it terminates the program with a non-zero
exit code and an error message. To achieve a similar result using Result
in
Rust, main
can be given a return type of Result
.
#include <stdexcept>
int main() {
throw std::runtime_error("oops");
}
fn main() -> Result<(), &'static str> {
Err("oops")
}
The result type must be unit ()
and the error type can be any type that
implements the Debug
trait.
#[derive(Debug)] struct InterestingError { message: &'static str, other_interesting_value: i32, } fn main() -> Result<(), InterestingError> { Err(InterestingError { message: "oops", other_interesting_value: 9001, }) }
Running this program produces the output Error: InterestingError { message: "oops", other_interesting_value: 9001 }
with an exit code of 1
.
Limitations to forcing error handling with Result
Returning Result
or Option
does not give the usual benefits when used with
APIs that pass pre-allocated buffers by mutable reference. This is because the
buffer is accessible outside of the Result
or Option
, and so the compiler
cannot force handling of the error case.
For example, in the following example the result of read_line
can be ignored,
resulting in logic errors in the program. However, since the buffer is required
to be initialized, it will not result in memory safety violations or undefined
behavior.
fn main() { let mut buffer = String::with_capacity(1024); std::io::stdin().read_line(&mut buffer); // use buffer }
Rust will produce a warning in this case, because of the #[must_use]
attribute
on Result
.
warning: unused `Result` that must be used
--> example.rs:3:5
|
3 | std::io::stdin().read_line(&mut buffer);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
3 | let _ = std::io::stdin().read_line(&mut buffer);
| +++++++
Option
does not have a #[must_use]
attribute, so functions that return an
Option
that must be handled (due to the None
case indicating an error)
should be annotated with the #[must_use]
attribute. For example, the get
method on slices returns Option
and is annotated as
#[must_use]
.