Errors and exceptions: C++ and Rust
C++ reports a failure by throwing an exception that is caught somewhere up the call stack, or by returning a value the caller is supposed to check. Rust reports it in the return type: the function returns Result<T, E>, and the caller has to do something with it before using the value. The two programs below parse a number and handle a bad one.
The task in C++
std::stoi returns an int and throws std::invalid_argument for text that is not a number. The first call succeeds, the second is inside a try block, and the catch prints the message. The exception type carries the reason, and the catch decides what to do.
The program prints the value and the caught message. The two paths are separate blocks, and the compiler does not require either of them to exist.
#include <iostream>
#include <stdexcept>
#include <string>
int parse(const std::string& text)
{
return std::stoi(text);
}
int main()
{
std::cout << "value " << parse("42") << '\n';
try {
std::cout << parse("x") << '\n';
} catch (const std::exception& error) {
std::cout << "caught " << error.what() << '\n';
}
}
g++ -std=c++23 -Wall -Wextra exceptions.cpp -o exceptions
./exceptions
value 42
caught stoi
The throw and the catch are connected at run time, not in the signature. A caller that forgets the try block still compiles, which is the mistake at the end of the page.
The same task in Rust
parse returns Result<i32, ParseIntError>, and the function maps the error to a String for its own signature. The caller matches the result twice and prints the value or the message.
The outputs are the number and the reason. The failure is a value, so it takes the same route as any other return value and cannot be skipped by the compiler.
fn parse(text: &str) -> Result<i32, String> {
text.parse().map_err(|error| format!("{error}"))
}
fn main() {
match parse("42") {
Ok(value) => println!("value {value}"),
Err(message) => println!("error {message}"),
}
match parse("x") {
Ok(value) => println!("value {value}"),
Err(message) => println!("error {message}"),
}
}
rustc --edition 2024 exceptions.rs
./exceptions
value 42
error invalid digit found in string
The match has two arms for each call, and the compiler checks that both are present. The lesson on Result shows the question mark that removes the repetition when the error just travels up.
What differs and why
Who carries the failure: in C++ the exception object is created on the throw and travels through a side channel, and the function signature says nothing about it. In Rust the error is the second variant of the return type, and every caller sees it in the signature.
What the compiler rejects: C++ accepts any call, with or without a handler, and an uncaught exception terminates the process. Rust rejects a use of the Result as if it were the value (E0308), so the error has to be handled, propagated with the question mark, or turned into a panic on purpose.
What happens at run time: an exception unwinds the stack and runs the destructors on the way, and if no handler is found the runtime calls std::terminate. Rust's question mark returns the Err value normally, so the stack unwinds by returning and nothing is lost.
The habit that breaks
The habit is to call a throwing function and use its result without a try block. The program below does exactly that, compiles, and aborts when the parse fails. The runtime prints the exception type and the message before it terminates.
#include <iostream>
#include <string>
int main()
{
int value = std::stoi("x");
std::cout << "value " << value << '\n';
}
g++ -std=c++23 -Wall -Wextra exceptions_wrong.cpp -o exceptions_wrong
./exceptions_wrong
terminate called after throwing an instance of 'std::invalid_argument'
what(): stoi
status 134
The Rust version of the same line assigns the Result to an integer variable. The compiler reports the two types and suggests nothing, because there is no implicit unwrap: the failure has to be handled on purpose.
fn main() {
let value: i32 = "x".parse();
println!("value {value}");
}
rustc --edition 2024 exceptions_wrong.rs
error[E0308]: mismatched types
--> exceptions_wrong.rs:2:22
|
2 | let value: i32 = "x".parse();
| --- ^^^^^^^^^^^ expected `i32`, found `Result<_, _>`
| |
| expected due to this
|
= note: expected type `i32`
found enum `Result<_, _>`
help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err`
|
2 | let value: i32 = "x".parse().expect("REASON");
| +++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
The C++ lesson for this ground is Exceptions, and the Rust lesson is Result and the question mark. The next page in this set is Inheritance and traits.
The C++ programs were built with g++ 15.2.0 and the Rust programs with rustc 1.96.1, and both were run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev