Skip to content
Learn Rust
Theme

Errors and Result

Data · Lesson 24

A function that can fail returns Result<T, E>: either Ok(value) or Err(error). The ? operator means "if this failed, return the error right now", in one character.

Coming from GoResult<T, E> replaces (T, error), and ? does what if err != nil { return err } does. Go is the closest cousin: errors are plain values that you return and check. The difference is that Rust won't hand you the value until you have dealt with the Err case, and warns when you ignore a Result altogether.

Coming from KotlinExpected failures are return values, not exceptions: Result<T, E> is a bit like Kotlin's Result from runCatching, but the error type is spelled out (Kotlin's is always a Throwable). ? passes the error up to the caller, and there is no try/catch for expected failures. Rust keeps panics for bugs.

Coming from PythonThere is no try/except for normal failures: the function returns Result<T, E>, either Ok(value) or Err(error). ? passes the error up the way an exception you don't catch would, but the possibility of failure shows in the function's type.

Coming from TypeScriptThere is no throw/catch for normal failures. A Result<T, E> is like returning a union of { ok: true, value } and { ok: false, error }, but it is built in, so the standard library and crates share the same one. The error type is in the signature (in strict mode a catch variable is unknown), and ? passes it up.

Coming from JavaResult<T, E> is a return value, not an exception, so the error shows up in the signature like a checked exception but travels like normal data. ? means "return the error to my caller now", which is what an uncaught checked exception does when your method declares throws.

Coming from C#Rust has no exceptions for normal errors: a function returns Result<T, E>, either Ok or Err. It's like the TryParse pattern built into the type system, and ? passes the error up to the caller in one character.

Coming from CResult<T, E> is C's "return an error code and write the value through a pointer", packed into one value. The compiler won't let you use the value without dealing with the error, and ? is the if (err) return err; line.

Coming from C++Result<T, E> replaces throwing: the error is part of the return type, and ? returns it early instead of unwinding. C++20's standard library has no equivalent; C++23's std::expected is the same idea.

The quick comparison

Rustsrc/main.rsRun ↗
use std::num::ParseIntError;
fn parse_age(s: &str) -> Result<u32, ParseIntError> {
let n: u32 = s.parse()?; // ? = if this failed, return the error now
Ok(n)
}
fn main() {
match parse_age("42") {
Ok(n) => println!("age {n}"),
Err(e) => println!("error: {e}"),
}
match parse_age("abc") {
Ok(n) => println!("age {n}"),
Err(e) => println!("error: {e}"),
}
}
$ cargo run
age 42
error: invalid digit found in string
package main
import (
"fmt"
"strconv"
)
func parseAge(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return n, nil
}
func main() {
for _, s := range []string{"42", "abc"} {
if n, err := parseAge(s); err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("age", n)
}
}
}
$ go run main.go
age 42
error: strconv.Atoi: parsing "abc": invalid syntax
fun parseAge(s: String): Int {
return s.toInt() // throws NumberFormatException on bad input
}
fun main() {
for (s in listOf("42", "abc")) {
try {
println("age ${parseAge(s)}")
} catch (e: NumberFormatException) {
println("error: ${e.message}")
}
}
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
age 42
error: For input string: "abc"
def parse_age(s: str) -> int:
return int(s) # raises ValueError on bad input; no ? needed
for text in ["42", "abc"]:
try:
print(f"age {parse_age(text)}")
except ValueError as e:
print(f"error: {e}")
$ python3 main.py
age 42
error: invalid literal for int() with base 10: 'abc'
function parseAge(s: string): number {
if (!/^\d+$/.test(s)) {
throw new Error("invalid digit found in string");
}
return Number(s);
}
for (const input of ["42", "abc"]) {
try {
console.log(`age ${parseAge(input)}`);
} catch (e) {
console.log(`error: ${(e as Error).message}`);
}
}
$ tsc --strict --module nodenext --types node main.ts && node main.js
age 42
error: invalid digit found in string
import java.util.List;
public class Main {
static int parseAge(String s) {
return Integer.parseInt(s); // throws NumberFormatException on bad input
}
public static void main(String[] args) {
for (var s : List.of("42", "abc")) {
try {
System.out.println("age " + parseAge(s));
} catch (NumberFormatException e) {
System.out.println("error: " + e.getMessage());
}
}
}
}
$ javac Main.java && java Main
age 42
error: For input string: "abc"
foreach (var input in new[] { "42", "abc" })
{
try
{
Console.WriteLine($"age {ParseAge(input)}");
}
catch (FormatException e)
{
Console.WriteLine($"error: {e.Message}");
}
}
// a bad string throws, and the exception flies up to the caller
static uint ParseAge(string s) => uint.Parse(s);
$ dotnet run
age 42
error: The input string 'abc' was not in a correct format.
#include <ctype.h>
#include <stdio.h>
// C has no Result: return an error message (NULL means success)
// and write the value through a pointer
const char *parse_age(const char *s, unsigned *out) {
if (*s == '\0') return "cannot parse integer from empty string";
unsigned n = 0;
for (; *s; s++) {
if (!isdigit((unsigned char)*s)) return "invalid digit found in string";
n = n * 10 + (unsigned)(*s - '0');
}
*out = n;
return NULL;
}
int main(void) {
const char *inputs[] = {"42", "abc"};
for (int i = 0; i < 2; i++) {
unsigned n;
const char *err = parse_age(inputs[i], &n);
if (err == NULL) {
printf("age %u\n", n);
} else {
printf("error: %s\n", err);
}
}
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
age 42
error: invalid digit found in string
#include <charconv>
#include <iostream>
#include <stdexcept>
#include <string_view>
unsigned parse_age(std::string_view s) {
unsigned n = 0;
auto [end, ec] = std::from_chars(s.data(), s.data() + s.size(), n);
if (ec != std::errc{} || end != s.data() + s.size()) {
throw std::invalid_argument("invalid digit found in string");
}
return n;
}
int main() {
for (std::string_view input : {"42", "abc"}) {
try {
unsigned n = parse_age(input); // a failure throws and jumps to catch
std::cout << "age " << n << "\n";
} catch (const std::invalid_argument& e) {
std::cout << "error: " << e.what() << "\n";
}
}
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
age 42
error: invalid digit found in string

What's different

  • Expected failures are ordinary return values, not exceptions, and the compiler won't let you use the value without dealing with the error. A panic! is the other kind of failure: it is for bugs, and it ends the thread it happens on (on the main thread, the program).
  • ? only works inside a function whose return type can carry the error: Result for a Result, Option for an Option. In main, write -> Result<(), Box<dyn Error>> to use it.
  • In real projects, the thiserror and anyhow crates (add-on libraries) remove most of the boilerplate: see Errors in real apps.
Gotcha

A Result is not the value

Looks fine: in Go, Kotlin, Python, TypeScript, Java, C# and C++ it runs fine; in C it runs but gives a wrong answer. Rust won't compile it.

Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.Broken in C. Rust catches it.Works in C++. In Rust it's just a rule.
Rustsrc/main.rs✗ won't compile
fn main() {
let n: u32 = "42".parse();
println!("{}", n + 1);
}
package main
import (
"fmt"
"strconv"
)
func main() {
n, _ := strconv.Atoi("42")
fmt.Println(n + 1)
}
$ go run main.go✓ runs
43
fun main() {
val n: Int = "42".toInt() // throws if the text isn't a number
println(n + 1)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
43
n = int("42")
print(n + 1)
$ python3 main.py✓ runs
43
const n: number = parseInt("42");
console.log(n + 1);
$ tsc --strict --module nodenext --types node main.ts && node main.js✓ runs
43
public class Main {
public static void main(String[] args) {
int n = Integer.parseInt("42");
System.out.println(n + 1);
}
}
$ javac Main.java && java Main✓ runs
43
var n = uint.Parse("42");
Console.WriteLine(n + 1);
$ dotnet run✓ runs
43
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = atoi("42");
int bad = atoi("abc");
printf("%d %d\n", n + 1, bad + 1);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main! runs · wrong answer
43 1
#include <iostream>
#include <string>
int main() {
int n = std::stoi("42");
std::cout << n + 1 << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
43
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:2:18
  |
2 |     let n: u32 = "42".parse();
  |            ---   ^^^^^^^^^^^^ expected `u32`, found `Result<_, _>`
  |            |
  |            expected due to this
  |
  = note: expected type `u32`
             found enum `Result<_, _>`
help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err`
  |
2 |     let n: u32 = "42".parse().expect("REASON");
  |                              +++++++++++++++++
What happens in Go

Go runs this and prints 43. The _ is written out, so ignoring the error is a visible choice, and for "42" it does no harm. With "abc" the error is thrown away, n is 0, and the program quietly prints 1.

What happens in Kotlin

Kotlin runs this and prints 43. With "abc", toInt() would throw a NumberFormatException, and nothing in the code warns you.

What happens in Python

Python prints 43. int("42") works, and if the text were "abc" it would raise a ValueError you didn't catch.

What happens in TypeScript

Prints 43. parseInt never throws: bad input like "abc" gives NaN, and the math carries on with it.

What happens in Java

This prints 43. NumberFormatException is unchecked, so nothing makes you handle it; with "abc" the same line would throw at runtime.

What happens in C#

uint.Parse hands back the number directly and prints 43. A bad string would throw a FormatException, which nothing here catches.

What happens in C

atoi has no way to report an error. It gives 42 for "42", but it also turns "abc" into 0, so the second number is a wrong 1.

What happens in C++

std::stoi hands back the number directly and prints 43. A bad input would throw an exception, but nothing in the types makes you handle it.

What Rust sees

parse returns Result<u32, ...>, not a u32. The compiler does not look at the text "42". It only knows that parsing text can fail.

Why Rust says no

The same line with a user's input could get "abc". Rust makes you choose what happens on failure. .unwrap_or(0) means "use 0 if it fails" on purpose (the same as Go's _), ? means "pass the error to my caller", and .unwrap() means "stop if wrong", which is what an uncaught exception does elsewhere, minus the stack trace.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
fn main() {
let n: u32 = "42".parse().unwrap_or(0); // 0 if it fails, chosen on purpose
println!("{}", n + 1);
}
$ cargo run
43

The toolkit

Two kinds of failure

Rust splits failures into two kinds and gives each its own tool.

Expected failure Bug
Examples The user typed abc where a number goes. A file is missing. An index past the end. A “can’t happen” that happened. A broken promise by the caller.
What happens The caller decides: retry, use a default, report it Nobody can fix it at run time, so the thread stops (on the main thread, the program)
Tool Return a Result panic!
In the signature Yes. The caller handles it or passes it up with ?; ignoring it draws a warning No

A value that is merely missing, such as a search that found nothing, is not a failure: that is Option, in the previous lesson. The rest of this lesson is mostly Result.

Result is an enum

Result<T, E> is an enum with two variants: Ok(T) holds the value when things worked, Err(E) holds the error when they did not. Enums showed how to take one apart with match. A Result is the same:

Rustsrc/main.rsRun ↗
fn main() {
// parse returns a Result: Ok(the number) or Err(what went wrong).
let good = "42".parse::<u32>();
let bad = "4x2".parse::<u32>();
println!("good: {good:?}");
println!("bad: {bad:?}");
println!("good.is_ok(): {}", good.is_ok());
println!("bad.is_err(): {}", bad.is_err());
// match opens a Result. The compiler checks that both cases are handled.
for text in ["42", "4x2", "", "-7", "99999999999"] {
match text.parse::<u32>() {
Ok(n) => println!("{text:?} -> number {n}"),
Err(e) => println!("{text:?} -> error: {e}"),
}
}
}
$ cargo run
good: Ok(42)
bad: Err(ParseIntError { kind: InvalidDigit })
good.is_ok(): true
bad.is_err(): true
"42" -> number 42
"4x2" -> error: invalid digit found in string
"" -> error: cannot parse integer from empty string
"-7" -> error: invalid digit found in string
"99999999999" -> error: number too large to fit in target type

parse can fail, so it returns a Result. You cannot use the number without opening it first, and match will not compile unless you handle both arms. The {e} and {:?} texts above come from the same ParseIntError.

?: pass the error up

Most functions can do nothing useful about an error except hand it to their caller. A match for that on every line is noisy, so Rust has ?:

Rustsrc/main.rsRun ↗
use std::num::ParseIntError;
// Each ? means: if this is an Err, return that Err from the whole function now.
// If it is Ok, take the value out and carry on.
fn total_cents(dollars: &str, cents: &str) -> Result<u32, ParseIntError> {
let d: u32 = dollars.parse()?;
let c: u32 = cents.parse()?;
Ok(d * 100 + c)
}
fn main() {
println!("total_cents(\"12\", \"50\"): {:?}", total_cents("12", "50"));
println!("total_cents(\"x\", \"50\"): {:?}", total_cents("x", "50"));
println!("total_cents(\"12\", \"\"): {:?}", total_cents("12", ""));
}
$ cargo run
total_cents("12", "50"): Ok(1250)
total_cents("x", "50"): Err(ParseIntError { kind: InvalidDigit })
total_cents("12", ""): Err(ParseIntError { kind: Empty })

After an expression that returns a Result, ? means: if it is Ok, take the value out and carry on. If it is Err, return that error from the whole function, right now. The happy path stays a straight line, and each ? marks a place where the function may return early.

Here is the same function with the ? written out as a match:

Rustsrc/main.rsRun ↗
use std::num::ParseIntError;
// The version with ?
fn short(text: &str) -> Result<u32, ParseIntError> {
let n: u32 = text.parse()?;
Ok(n + 1)
}
// What ? does, written out. The Err arm calls From::from on the error:
// when the types already match, that changes nothing.
fn long(text: &str) -> Result<u32, ParseIntError> {
let n: u32 = match text.parse() {
Ok(value) => value,
Err(e) => return Err(From::from(e)),
};
Ok(n + 1)
}
fn main() {
for text in ["41", "forty-one"] {
println!("short({text:?}): {:?}", short(text));
println!("long({text:?}): {:?}", long(text));
}
}
$ cargo run
short("41"): Ok(42)
long("41"): Ok(42)
short("forty-one"): Err(ParseIntError { kind: InvalidDigit })
long("forty-one"): Err(ParseIntError { kind: InvalidDigit })

Both print the same. The Err arm calls From::from(e) before returning. From is the standard way to say “make this type out of that one”. Here the error is already a ParseIntError, so it changes nothing.

When the error types differ

The error ? finds has to become the error type in your signature. If Rust has no conversion between the two, you get E0277:

Rustsrc/main.rs✗ won't compile
// The function says its error is a String, but parse gives a ParseIntError.
fn double(text: &str) -> Result<u32, String> {
let n: u32 = text.parse()?;
Ok(n * 2)
}
fn main() {
println!("{:?}", double("21"));
}
$ cargo buildWhat is E0277?
error[E0277]: `?` couldn't convert the error to `String`
 --> src/main.rs:3:30
  |
2 | fn double(text: &str) -> Result<u32, String> {
  |                          ------------------- expected `String` because of this
3 |     let n: u32 = text.parse()?;
  |                       -------^ the trait `From<ParseIntError>` is not implemented for `String`
  |                       |
  |                       this can't be annotated with `?` because it has type `Result<_, ParseIntError>`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = help: `String` implements trait `From<T>`:
            From<&String>
            From<&mut str>
            From<&str>
            From<Box<str>>
            From<Cow<'_, str>>
            From<char>

The message says it: ? converts through the From trait, and String has no From<ParseIntError>. Three ways out:

  • map_err: change the error by hand at that spot (below).
  • A From impl for your own error type, so ? converts by itself (see Your own error type).
  • Box<dyn Error> as the error type, which accepts any error type (see the catch-all).

? needs a function that returns a Result

? returns the error, so the function’s return type must be able to hold one. main returns () by default:

Rustsrc/main.rs✗ won't compile
fn main() {
let n: u32 = "42".parse()?;
println!("n: {n}");
}
$ cargo buildWhat is E0277?
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:2:30
  |
1 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
2 |     let n: u32 = "42".parse()?;
  |                              ^ cannot use the `?` operator in a function that returns `()`
  |
help: consider adding return type
  |
1 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
2 |     let n: u32 = "42".parse()?;
3 |     println!("n: {n}");
4 +     Ok(())

The help: shows the fix: give main a return type (below). ? also works on an Option inside a function that returns Option, as the Option lesson shows.

Cleanup still happens

An early return from ? does not skip cleanup. A value with a Drop impl (code that runs when the value goes out of scope, from Ownership) is cleaned up on both paths:

Rustsrc/main.rsRun ↗
use std::num::ParseIntError;
// Drop is code that runs when a value goes out of scope (the Traits lesson
// covers it). It plays the part of finally, a destructor, or a deferred call.
struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) {
println!("cleanup: {}", self.0);
}
}
fn work(text: &str) -> Result<u32, ParseIntError> {
let _guard = Guard("work");
let n: u32 = text.parse()?; // on an Err, the function returns from here
println!("parsed {n}");
Ok(n)
}
fn main() {
println!("work(\"7\"): {:?}", work("7"));
println!("work(\"x\"): {:?}", work("x"));
}
$ cargo run
parsed 7
cleanup: work
work("7"): Ok(7)
cleanup: work
work("x"): Err(ParseIntError { kind: InvalidDigit })

This does the job of finally, a destructor or defer. One limit: drop cannot return an error, so if closing or flushing a file can fail, call the type’s flush yourself and use ?.

Do not ignore a Result

If you call a function that returns a Result and do nothing with it, Rust warns you: Result is marked #[must_use]. By default that is only a warning and the program still builds. To show the message on this page, the program below turns that one warning into an error:

Rustsrc/main.rs✗ won't compile
// By default an ignored Result is a warning. This line turns it into an error,
// so the page can show the message.
#![deny(unused_must_use)]
fn save(name: &str) -> Result<(), String> {
if name.is_empty() {
Err("empty name".to_string())
} else {
Ok(())
}
}
fn main() {
save("");
// Ignoring on purpose is allowed. Say so with let _ =.
let _ = save("");
println!("done");
}
$ cargo build
error: unused `Result` that must be used
  --> src/main.rs:14:5
   |
14 |     save("");
   |     ^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
note: the lint level is defined here
  --> src/main.rs:3:9
   |
 3 | #![deny(unused_must_use)]
   |         ^^^^^^^^^^^^^^^
help: use `let _ = ...` to ignore the resulting value
   |
14 |     let _ = save("");
   |     +++++++

let _ = save(""); says “I know, and I do not care”. It is allowed and visible in the code, and only the first call is flagged.

Panics: when the thread stops

A panic stops the code at the spot where something impossible happened. The most common source is unwrap, which takes the Ok value out of a Result and panics on Err:

Rustsrc/main.rs! panics at runtimeRun ↗
fn main() {
println!("before");
let age: u32 = "forty".parse().unwrap();
println!("age: {age}");
}
$ cargo run
before

thread 'main' (1) panicked at src/main.rs:3:36:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Read it from the top. thread 'main' (1) panicked at src/main.rs:3:36: gives the thread (main, then its id number, which differs between runs) and the place: line 3, column 36. The next line is the message; for unwrap it shows the error’s Debug form (the {:?} text). The note: says that setting RUST_BACKTRACE=1 makes a panic also print a backtrace, the calls that led here (the Book, chapter 9.1). A panic on the main thread ends the program with exit code 101 (with the default unwinding, below).

By default a panic unwinds: Rust walks back up the call stack and cleans up each function’s values. The Book describes a panic = "abort" setting in Cargo.toml that skips the cleanup and ends the program at once.

A panic ends only the thread it happens on. A spawned thread that panics leaves the rest of the program running, and the thread waiting for it gets an Err from join (threads come later):

Rustsrc/main.rsRun ↗
use std::thread;
// A thread is a second line of execution (a later lesson). Read the closure
// as "run this on another thread".
fn main() {
let worker = thread::spawn(|| {
panic!("the worker hit a bug");
});
let outcome = worker.join();
println!("the worker panicked: {}", outcome.is_err());
println!("main is still running");
}
$ cargo run
thread '<unnamed>' (1) panicked at src/main.rs:7:9:
the worker hit a bug
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
the worker panicked: true
main is still running

std::panic::catch_unwind can catch a panic, but its docs say not to use it as a general try/catch.

Panics also come from indexing past the end, a failed assert! (checked in every build, release too), unreachable!(), todo!(), and your own panic!("message"). Here is a panic that is the right call: the function has a rule (the slice must not be empty), and a caller who breaks it has a bug:

Rustsrc/main.rs! panics at runtimeRun ↗
// The caller must pass a non-empty slice. An empty one is a bug in the caller,
// not bad input to recover from, so the function stops loudly.
fn average(scores: &[u32]) -> u32 {
assert!(!scores.is_empty(), "average needs at least one score");
scores.iter().sum::<u32>() / scores.len() as u32
}
fn main() {
println!("average: {}", average(&[70, 80, 90]));
println!("average: {}", average(&[]));
}
$ cargo run
average: 80

thread 'main' (1) panicked at src/main.rs:4:5:
average needs at least one score
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The Book’s rule of thumb: return a Result when failure can happen in a correct program, such as bad input. Panic when an assumption or promise is broken, because that is a bug, and a caller should not have to handle a bug (chapter 9.3). If an empty list can come from user data, return an Option or Result instead.

unwrap and expect

unwrap says “I am sure this worked, and if I am wrong, stop.” It is what an uncaught exception does elsewhere, minus the traceback. expect("why") adds your message in front of the error, so the panic explains your assumption:

Rustsrc/main.rs! panics at runtimeRun ↗
fn main() {
let text = "forty";
// expect is unwrap with your own message in front of the error.
let age: u32 = text.parse().expect("age should be a whole number");
println!("age: {age}");
}
$ cargo run
thread 'main' (1) panicked at src/main.rs:4:33:
age should be a whole number: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Both are fine in some places:

Rustsrc/main.rsRun ↗
use std::net::Ipv4Addr;
fn main() {
// The text is a literal in this file, and a valid address. If someone mistypes
// it later, the program stops at once and the message says what was meant.
let home: Ipv4Addr = "127.0.0.1".parse().expect("the literal is a valid address");
println!("home: {home}");
// A fallback chosen on purpose is not a crash waiting to happen.
let width: u32 = "wide".parse().unwrap_or(80);
println!("width: {width}");
}
$ cargo run
home: 127.0.0.1
width: 80
  • A value you can check by reading the code, like the hard-coded address. Use expect and say why it cannot fail.
  • Tests, examples and quick prototypes, where a panic is the failure you want. The Book says this too.
  • A chosen fallback with unwrap_or(80) is not a crash at all.

On real input, unwrap waits for the wrong input. Use ? or match there.

Changing and combining results

Small methods on Result cover most of what you would otherwise match for. |e| … is a closure, a small unnamed function (Closures come later). Read it as “given e, do this”.

Rustsrc/main.rsRun ↗
use std::collections::HashMap;
// A HashMap is Rust's map type (Maps is a later lesson). get returns an Option.
fn port_from(settings: &HashMap<&str, &str>) -> Result<u16, String> {
let text = settings
.get("port")
.ok_or_else(|| "no 'port' setting".to_string())?; // Option -> Result
let port: u16 = text.parse().map_err(|e| format!("port {text:?}: {e}"))?; // change the error
if port < 1024 {
return Err(format!("port {port} needs special rights"));
}
Ok(port)
}
// and_then: run a second step that can also fail, only if the first worked.
fn half(n: u32) -> Result<u32, String> {
if n % 2 == 0 {
Ok(n / 2)
} else {
Err(format!("{n} is odd"))
}
}
fn main() {
let mut settings = HashMap::new();
println!("empty: {:?}", port_from(&settings));
settings.insert("port", "http");
println!("http: {:?}", port_from(&settings));
settings.insert("port", "80");
println!("80: {:?}", port_from(&settings));
settings.insert("port", "8080");
println!("8080: {:?}", port_from(&settings));
println!("half(8): {:?}", half(8));
println!("half(8) then half: {:?}", half(8).and_then(half));
println!("half(6) then half: {:?}", half(6).and_then(half));
println!("half(7) then half: {:?}", half(7).and_then(half));
}
$ cargo run
empty: Err("no 'port' setting")
http: Err("port \"http\": invalid digit found in string")
80: Err("port 80 needs special rights")
8080: Ok(8080)
half(8): Ok(4)
half(8) then half: Ok(2)
half(6) then half: Err("3 is odd")
half(7) then half: Err("7 is odd")
  • ok_or(error) turns an Option into a Result: None becomes Err(error). ok_or_else(|| …) builds the error only when needed, which matters when that costs something, such as making a String.
  • map_err(f) rewrites the error and leaves an Ok alone. Use it to add context or switch to your own error type.
  • and_then(f) runs the next step only if the last one worked. The step returns a Result itself, so failures chain without nesting.

More methods. The inputs are Ok(4) and Err("bad"), with String as the error type:

Method Does On Ok(4) On Err("bad")
map(|n| n * 10) change the Ok value Ok(40) Err("bad")
map_err(|e| e.len()) change the Err value Ok(4) Err(3)
unwrap_or(0) value, or a fallback 4 0
unwrap_or_else(|e| e.len() as i32) value, or compute a fallback from the error 4 3
ok() drop the error: becomes an Option Some(4) None
is_ok() / is_err() ask which one true / false false / true
Rustsrc/main.rsRun ↗
// Backs the table of Result methods in the lesson.
fn main() {
let ok: Result<i32, String> = Ok(4);
let err: Result<i32, String> = Err("bad".to_string());
assert_eq!(ok.clone().map(|n| n * 10), Ok(40));
assert_eq!(err.clone().map(|n| n * 10), Err("bad".to_string()));
assert_eq!(ok.clone().map_err(|e| e.len()), Ok(4));
assert_eq!(err.clone().map_err(|e| e.len()), Err(3));
assert_eq!(
ok.clone().and_then(|n| if n > 5 {
Ok(n)
} else {
Err("small".to_string())
}),
Err("small".to_string())
);
assert_eq!(
err.clone().and_then(|n| Ok::<i32, String>(n + 1)),
Err("bad".to_string())
);
assert_eq!(ok.clone().unwrap_or(0), 4);
assert_eq!(err.clone().unwrap_or(0), 0);
assert_eq!(err.clone().unwrap_or_else(|e| e.len() as i32), 3);
assert_eq!(err.clone().unwrap_or_default(), 0);
assert_eq!(ok.clone().ok(), Some(4));
assert_eq!(err.clone().ok(), None);
assert_eq!(err.clone().err(), Some("bad".to_string()));
assert_eq!(Some(4).ok_or("none"), Ok(4));
assert_eq!(None::<i32>.ok_or("none"), Err("none"));
assert_eq!(ok.clone().unwrap_or_else(|e| e.len() as i32), 4);
assert_eq!(ok.clone().unwrap_or_default(), 4);
assert_eq!(ok.clone().err(), None);
assert!(ok.is_ok() && !ok.is_err());
assert!(err.is_err() && !err.is_ok());
println!("all checks passed");
}

Collecting results

To parse a whole list, a for loop with ? works. An iterator’s collect can also build a Result<Vec<_>, _> from items that are each a Result: Ok with every value, or the first Err.

Rustsrc/main.rsRun ↗
use std::num::ParseIntError;
// Parse every piece, or stop at the first bad one. A for loop with ? does it.
fn parse_all(text: &str) -> Result<Vec<i32>, ParseIntError> {
let mut numbers = Vec::new();
for piece in text.split(',') {
numbers.push(piece.trim().parse()?);
}
Ok(numbers)
}
// The same, as one expression. collect can build a Result<Vec<_>, _> from
// an iterator of Results: the Vec if all are Ok, or the first Err.
fn parse_all_collect(text: &str) -> Result<Vec<i32>, ParseIntError> {
text.split(',').map(|piece| piece.trim().parse()).collect()
}
fn main() {
for text in ["1, 2, 3", "1, two, 3", ""] {
println!("loop {text:?}: {:?}", parse_all(text));
println!("collect {text:?}: {:?}", parse_all_collect(text));
}
}
$ cargo run
loop    "1, 2, 3": Ok([1, 2, 3])
collect "1, 2, 3": Ok([1, 2, 3])
loop    "1, two, 3": Err(ParseIntError { kind: InvalidDigit })
collect "1, two, 3": Err(ParseIntError { kind: InvalidDigit })
loop    "": Err(ParseIntError { kind: Empty })
collect "": Err(ParseIntError { kind: Empty })

Note the last row: "" splits into one empty piece, which is not a number. The iterators lesson explains map and collect.

Your own error type

A String error works for a first draft, but a caller cannot tell failures apart without comparing text. The usual answer is an enum with one variant per way to fail. Variants can carry data, as Enums showed:

Rustsrc/main.rsRun ↗
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
// One enum lists every way reading the setting can fail.
#[derive(Debug)]
enum ConfigError {
Missing,
NotANumber(ParseIntError),
OutOfRange { value: u32, max: u32 },
}
// Display is the message a person reads. It describes this level only.
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::Missing => write!(f, "the threads setting is missing"),
ConfigError::NotANumber(_) => write!(f, "the threads setting is not a number"),
ConfigError::OutOfRange { value, max } => {
write!(f, "threads is {value}, but the most allowed is {max}")
}
}
}
}
// Error needs Debug and Display. source says which error caused this one.
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConfigError::NotANumber(cause) => Some(cause),
_ => None,
}
}
}
// This is the conversion that ? calls when it finds a ParseIntError in a
// function that returns ConfigError.
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> ConfigError {
ConfigError::NotANumber(e)
}
}
fn threads(setting: Option<&str>) -> Result<u32, ConfigError> {
let text = setting.ok_or(ConfigError::Missing)?;
let n: u32 = text.parse()?;
if n > 64 {
return Err(ConfigError::OutOfRange { value: n, max: 64 });
}
Ok(n)
}
fn main() {
for setting in [Some("8"), None, Some("many"), Some("500")] {
match threads(setting) {
Ok(n) => println!("ok: {n} threads"),
Err(e) => {
println!("Display: {e}");
println!("Debug: {e:?}");
if let Some(cause) = e.source() {
println!("cause: {cause}");
}
}
}
}
}
$ cargo run
ok: 8 threads
Display: the threads setting is missing
Debug:   Missing
Display: the threads setting is not a number
Debug:   NotANumber(ParseIntError { kind: InvalidDigit })
cause:   invalid digit found in string
Display: threads is 500, but the most allowed is 64
Debug:   OutOfRange { value: 500, max: 64 }

Four parts:

  • #[derive(Debug)] gives the Debug: lines, the shape of the value for programmers.
  • impl fmt::Display for ConfigError writes the message a person reads, the Display: lines (what {} prints). An impl block holds the functions of a type, as in Methods. This one reads impl Trait for Type: a trait is a named set of functions a type promises to provide, much like an interface, and Display is the one {} asks for (Traits covers them). For now read it as “here is how to print a ConfigError”. write! is println! that writes into f. fmt::Result is a short name for Result<(), fmt::Error>.
  • impl Error for ConfigError marks the type as an error, which Box<dyn Error> and other tools need. It requires Debug and Display. Its source method is optional: it returns the error that caused this one, or None. Copy that signature as written. A message describes its own level and leaves the cause to source, so the cause is not printed twice.
  • impl From<ParseIntError> for ConfigError is the function ? calls. Because it exists, text.parse()? compiles in a function that returns Result<_, ConfigError>, and NotANumber is built for you. When two failures of one type need different context, use map_err instead.

The match in fmt has no _ arm, so a new variant makes the compiler list every match that needs one.

Box<dyn Error>: the catch-all

When you do not care which errors there are, only that they are errors (a small tool, a script, main), the error type can be Box<dyn Error> (in a library’s API):

Rustsrc/main.rsRun ↗
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
struct TooBig(u32);
impl fmt::Display for TooBig {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} is too big", self.0)
}
}
impl Error for TooBig {}
// Box<dyn Error> holds any error. ? converts into it, whatever the type.
fn check(text: &str) -> Result<u32, Box<dyn Error>> {
if text.is_empty() {
return Err("nothing to check".into()); // a &str becomes an error
}
let n: u32 = text.parse()?; // a ParseIntError
if n > 100 {
return Err(Box::new(TooBig(n))); // our own error type
}
Ok(n)
}
fn main() {
for text in ["42", "", "x", "500"] {
match check(text) {
Ok(n) => println!("{text:?} -> ok {n}"),
Err(e) => {
// The concrete type is hidden, but you can ask for it back.
let is_parse = e.downcast_ref::<ParseIntError>().is_some();
println!("{text:?} -> {e} (a ParseIntError: {is_parse})");
}
}
}
}
$ cargo run
"42" -> ok 42
"" -> nothing to check (a ParseIntError: false)
"x" -> invalid digit found in string (a ParseIntError: true)
"500" -> 500 is too big (a ParseIntError: false)

Read Box<dyn Error> as “some error, of a type decided when the program runs”. dyn marks a type not fixed at compile time. A Box is a pointer to a value on the heap, and the value is any type that implements Error. (Trait objects get their own lesson later; the picture below shows what is inside.) ? turns any type that implements Error into one. So does .into(), which is From seen from the other side, and it also turns a &str or String into one.

The price is that the type is hidden. downcast_ref asks “was it this one?”, but a match on an enum is nicer. Many projects use their own enum in libraries and Box<dyn Error> in applications.

main can return a Result

Give main the return type Result<(), Box<dyn Error>> and you can use ? all through the program:

Rustsrc/main.rsRun ↗
use std::error::Error;
fn parse_port(text: &str) -> Result<u16, std::num::ParseIntError> {
text.parse()
}
// main may return a Result. An Err ends the program with a message and a
// non-zero exit code. Here every step works, so it prints and returns Ok.
fn main() -> Result<(), Box<dyn Error>> {
let port = parse_port("8080")?;
println!("port: {port}");
Ok(())
}
$ cargo run
port: 8080

When main returns an Err, the program prints Error: and the error’s Debug form, then exits with a failure code:

Rustsrc/main.rs! exits with code 1Run ↗
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
println!("starting");
let port: u16 = "http".parse()?;
println!("port: {port}");
Ok(())
}
$ cargo run
starting
Error: ParseIntError { kind: InvalidDigit }

Note what is not there: no stack trace, and RUST_BACKTRACE=1 does not add one (that variable is for panics). A plain error value does not record where it was created. That is a real cost when debugging, and why people add context as errors travel up.

Exit codes

An exit code tells the shell how a program ended: 0 means success. This program starts copies of itself to show the codes:

Rustsrc/main.rsRun ↗
use std::error::Error;
use std::process::{Command, exit};
// This program starts copies of itself and prints how each one ended.
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(String::as_str) {
Some("ok") => println!("fine"),
Some("panic") => panic!("a bug"),
Some("exit") => exit(2),
Some("return-err") => {
let n: u32 = "x".parse()?;
println!("unreachable {n}");
}
_ => {
// No argument: this is the parent. Start one child per mode.
for mode in ["ok", "panic", "exit", "return-err"] {
let me = std::env::current_exe()?;
let status = Command::new(me).arg(mode).output()?.status;
println!("{mode}: exit code {:?}", status.code());
}
}
}
Ok(())
}
$ cargo run
ok: exit code Some(0)
panic: exit code Some(101)
exit: exit code Some(2)
return-err: exit code Some(1)
How the program ends Exit code
main returns normally (Ok) 0
main returns Err 1
a panic on the main thread (default unwinding) 101
std::process::exit(2) 2

std::process::exit(code) ends the program on the spot, and its docs warn that no destructors run. Prefer returning from main, which can return an ExitCode to pick the code. The table is what the program printed (Linux on ARM64, Rust 1.99.1).

Your language’s error handling, in Rust

Going deeper

What ? really does

The match earlier is a simplified ?. The Reference defines ? through a trait named Try, which is unstable, so you cannot implement it for your own types yet. For a Result its rules are the ones used here: Ok(val) evaluates to val, and Err(e) returns Err(From::from(e)). The same operator also covers Option and a few other types (Reference).

The conversion is why Box<dyn Error> accepts any type that implements Error: it has a From impl for those types (and for &str and String), which the ? in boxed.rs uses.

What a Result looks like in memory

A Result is an enum, so it is stored as the enums lesson showed: a tag for the variant, and room for the biggest payload. A Result<u32, ParseIntError> lives entirely on the stack:

let ok = "42".parse::<u32>(); let bad = "4x2".parse::<u32>();
STACKok: Result<u32, ParseIntError>tag: Oku32: 42bad: Result<u32, ParseIntError>tag: Errkind: InvalidDigit
Both are the same size, and no heap memory is used. (The exact bytes are the compiler's choice; the program below prints the size.)

A Box<dyn Error> is two pointers wide, because it must say both where the error is and what type it is:

let boxed: Box<dyn Error> = Box::new("x".parse::<u32>().unwrap_err());
STACKHEAPboxed: Box<dyn Error>data ptrvtable ptrthe vtable is a table of functions for this type, including its Error and Display onesa ParseIntErrorkind: InvalidDigit
The data pointer finds the error on the heap. The vtable pointer finds the functions to call on it, so {e} works without knowing the type. size_of below says 16 bytes: two 8-byte pointers on this 64-bit platform.
Rustsrc/main.rsRun ↗
use std::error::Error;
use std::mem::{size_of, size_of_val};
use std::num::ParseIntError;
fn main() {
println!("size_of u32: {}", size_of::<u32>());
println!("size_of ParseIntError: {}", size_of::<ParseIntError>());
println!(
"size_of Result<u32, ParseIntError>: {}",
size_of::<Result<u32, ParseIntError>>()
);
println!("size_of &dyn Error: {}", size_of::<&dyn Error>());
println!("size_of Box<dyn Error>: {}", size_of::<Box<dyn Error>>());
println!(
"size_of Result<(), Box<dyn Error>>: {}",
size_of::<Result<(), Box<dyn Error>>>()
);
// The box holds the error value somewhere else. This is how big that value is.
let boxed: Box<dyn Error> = Box::new("x".parse::<u32>().unwrap_err());
println!(
"size of the ParseIntError inside the box: {}",
size_of_val(&*boxed)
);
}
$ cargo run
size_of u32: 4
size_of ParseIntError: 1
size_of Result<u32, ParseIntError>: 8
size_of &dyn Error: 16
size_of Box<dyn Error>: 16
size_of Result<(), Box<dyn Error>>: 16
size of the ParseIntError inside the box: 1

The sizes show it: Box<dyn Error> is 16 bytes, and Result<(), Box<dyn Error>> is also 16, because the pointer is never null, so the Ok case fits in the null value (a niche; Enums has the details). The ParseIntError inside the box is 1 byte. Boxing a value that has a size takes a heap allocation.

Error chains and source

An error often has a cause: “could not load limits.txt”, because “bad value for threads”, because “invalid digit found in string”. Each error returns the one below it from source(), and custom-error.rs already did this for NotANumber. This program walks a two-level chain, then does what Go’s errors.As does: look for a ParseIntError at any depth.

Rustsrc/main.rsRun ↗
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
struct SettingError {
key: String,
cause: ParseIntError,
}
impl fmt::Display for SettingError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "bad value for {}", self.key)
}
}
impl Error for SettingError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.cause)
}
}
#[derive(Debug)]
struct LoadError {
file: String,
cause: SettingError,
}
impl fmt::Display for LoadError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "could not load {}", self.file)
}
}
impl Error for LoadError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.cause)
}
}
// Rust's version of Go's errors.As: walk the chain and test each error.
// (Copy the signature as written; Box<dyn Error> is explained above.)
fn find_parse_error<'a>(err: &'a (dyn Error + 'static)) -> Option<&'a ParseIntError> {
let mut current = Some(err);
while let Some(e) = current {
if let Some(found) = e.downcast_ref::<ParseIntError>() {
return Some(found);
}
current = e.source();
}
None
}
fn main() {
let err = LoadError {
file: "limits.txt".to_string(),
cause: SettingError {
key: "threads".to_string(),
cause: "12k".parse::<u32>().unwrap_err(),
},
};
println!("top: {err}");
let mut current = err.source();
let mut depth = 1;
while let Some(cause) = current {
println!("{depth} level down: {cause}");
current = cause.source();
depth += 1;
}
println!(
"top level is a ParseIntError: {}",
(&err as &dyn Error)
.downcast_ref::<ParseIntError>()
.is_some()
);
println!("found one in the chain: {:?}", find_parse_error(&err));
}
$ cargo run
top: could not load limits.txt
1 level down: bad value for threads
2 level down: invalid digit found in string
top level is a ParseIntError: false
found one in the chain: Some(ParseIntError { kind: InvalidDigit })

Testing only the top-level error finds nothing, because downcast_ref and matches! look at one layer. The loop over source() finds the ParseIntError. It is the same shape as Go’s errors.Unwrap, Java’s getCause, C#’s InnerException and Python’s __cause__. The std docs say a cause should be reached through source() or shown in Display, but not both.

Practice

Try it

Each exercise has checks in main. Open it in the Playground, replace the todo!(), and run it until it prints all checks passed. A plain for loop is fine.

Parse a range

Write parse_range, which turns "3-7" into Ok((3, 7)), with an Err message for each failure: no - ("missing '-'"), a side that is not a whole number (starting "bad start" or "bad end"), and a start after the end ("start is after end"). Use split_once, ok_or, map_err and ?. Spaces are fine, and an empty side such as "-7" is an error.

Starterexercises/parse-range.rs
/// Turn "3-7" into Ok((3, 7)). Spaces around either number are fine.
/// (Hint: ok_or("message")? works because a &str converts into a String error.)
/// Return Err with a short message when:
/// - there is no '-' ("missing '-'")
/// - either side is not a whole number (a message starting "bad start" or "bad end")
/// - the start is bigger than the end ("start is after end")
fn parse_range(text: &str) -> Result<(u32, u32), String> {
todo!("split_once, then ok_or and map_err with ?, then compare")
}
fn main() {
assert_eq!(parse_range("3-7"), Ok((3, 7)));
assert_eq!(parse_range(" 3 - 7 "), Ok((3, 7)));
assert_eq!(parse_range("5-5"), Ok((5, 5)));
assert_eq!(parse_range("7"), Err("missing '-'".to_string()));
assert_eq!(parse_range("7-3"), Err("start is after end".to_string()));
assert_eq!(parse_range(""), Err("missing '-'".to_string()));
// "-7" has an empty start, and "3-4-5" has "4-5" as the end. The Arabic-Indic
// digits in the last one are numbers to people, but not to parse.
for bad in ["3-", "-7", "a-b", "3-4-5", "٣-٥"] {
let message = parse_range(bad).unwrap_err();
assert!(message.starts_with("bad "), "{bad:?} gave {message:?}");
}
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/parse-range.solution.rsRun ↗
/// Turn "3-7" into Ok((3, 7)). Spaces around either number are fine.
/// (Hint: ok_or("message")? works because a &str converts into a String error.)
/// Return Err with a short message when:
/// - there is no '-' ("missing '-'")
/// - either side is not a whole number (a message starting "bad start" or "bad end")
/// - the start is bigger than the end ("start is after end")
fn parse_range(text: &str) -> Result<(u32, u32), String> {
let (left, right) = text.split_once('-').ok_or("missing '-'")?;
let start: u32 = left
.trim()
.parse()
.map_err(|e| format!("bad start {left:?}: {e}"))?;
let end: u32 = right
.trim()
.parse()
.map_err(|e| format!("bad end {right:?}: {e}"))?;
if start > end {
return Err("start is after end".to_string());
}
Ok((start, end))
}
fn main() {
assert_eq!(parse_range("3-7"), Ok((3, 7)));
assert_eq!(parse_range(" 3 - 7 "), Ok((3, 7)));
assert_eq!(parse_range("5-5"), Ok((5, 5)));
assert_eq!(parse_range("7"), Err("missing '-'".to_string()));
assert_eq!(parse_range("7-3"), Err("start is after end".to_string()));
assert_eq!(parse_range(""), Err("missing '-'".to_string()));
// "-7" has an empty start, and "3-4-5" has "4-5" as the end. The Arabic-Indic
// digits in the last one are numbers to people, but not to parse.
for bad in ["3-", "-7", "a-b", "3-4-5", "٣-٥"] {
let message = parse_range(bad).unwrap_err();
assert!(message.starts_with("bad "), "{bad:?} gave {message:?}");
}
println!("all checks passed");
}
$ cargo run
all checks passed

Check a username

Finish the Display impl for UsernameError, then write check_username. A name is 1 to 12 characters, each alphanumeric (letters and numbers of any script count) or _. Count characters, not bytes: twelve é are 24 bytes and still a valid name.

Starterexercises/username.rs
use std::error::Error;
use std::fmt;
#[derive(Debug, PartialEq)]
enum UsernameError {
Empty,
TooLong(usize),
BadChar(char),
}
// Write the messages: "username is empty", "username has 13 characters, the
// most allowed is 12", and "username contains '!'" (with the character).
impl fmt::Display for UsernameError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
todo!("match on self and write! a message into f")
}
}
impl Error for UsernameError {}
/// A username has 1 to 12 characters, and each one is alphanumeric (letters and
/// numbers of any script count) or '_'.
/// Count characters, not bytes. Check for an empty name first, then the length,
/// then the characters.
fn check_username(name: &str) -> Result<&str, UsernameError> {
todo!("check empty, then the character count, then each character")
}
fn main() {
assert_eq!(check_username("ada_99"), Ok("ada_99"));
assert_eq!(check_username(""), Err(UsernameError::Empty));
assert_eq!(check_username("a b"), Err(UsernameError::BadChar(' ')));
// 12 characters but 24 bytes: fine. 13 characters: too long.
assert_eq!(check_username("éééééééééééé"), Ok("éééééééééééé"));
assert_eq!(
check_username("ééééééééééééé"),
Err(UsernameError::TooLong(13))
);
assert_eq!(check_username("josé!"), Err(UsernameError::BadChar('!')));
assert_eq!(UsernameError::Empty.to_string(), "username is empty");
assert_eq!(
UsernameError::TooLong(13).to_string(),
"username has 13 characters, the most allowed is 12"
);
assert_eq!(
UsernameError::BadChar('!').to_string(),
"username contains '!'"
);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/username.solution.rsRun ↗
use std::error::Error;
use std::fmt;
#[derive(Debug, PartialEq)]
enum UsernameError {
Empty,
TooLong(usize),
BadChar(char),
}
// Write the messages: "username is empty", "username has 13 characters, the
// most allowed is 12", and "username contains '!'" (with the character).
impl fmt::Display for UsernameError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
UsernameError::Empty => write!(f, "username is empty"),
UsernameError::TooLong(n) => {
write!(f, "username has {n} characters, the most allowed is 12")
}
UsernameError::BadChar(c) => write!(f, "username contains {c:?}"),
}
}
}
impl Error for UsernameError {}
/// A username has 1 to 12 characters, and each one is alphanumeric (letters and
/// numbers of any script count) or '_'.
/// Count characters, not bytes. Check for an empty name first, then the length,
/// then the characters.
fn check_username(name: &str) -> Result<&str, UsernameError> {
if name.is_empty() {
return Err(UsernameError::Empty);
}
let length = name.chars().count();
if length > 12 {
return Err(UsernameError::TooLong(length));
}
for c in name.chars() {
if !(c.is_alphanumeric() || c == '_') {
return Err(UsernameError::BadChar(c));
}
}
Ok(name)
}
fn main() {
assert_eq!(check_username("ada_99"), Ok("ada_99"));
assert_eq!(check_username(""), Err(UsernameError::Empty));
assert_eq!(check_username("a b"), Err(UsernameError::BadChar(' ')));
// 12 characters but 24 bytes: fine. 13 characters: too long.
assert_eq!(check_username("éééééééééééé"), Ok("éééééééééééé"));
assert_eq!(
check_username("ééééééééééééé"),
Err(UsernameError::TooLong(13))
);
assert_eq!(check_username("josé!"), Err(UsernameError::BadChar('!')));
assert_eq!(UsernameError::Empty.to_string(), "username is empty");
assert_eq!(
UsernameError::TooLong(13).to_string(),
"username has 13 characters, the most allowed is 12"
);
assert_eq!(
UsernameError::BadChar('!').to_string(),
"username contains '!'"
);
println!("all checks passed");
}
$ cargo run
all checks passed

Sum a list of numbers

Write sum_numbers, which adds up "1, 2, 3". Return a Box<dyn Error> for a bad piece and for a sum that overflows an i32 (see checked_add). An empty string has no numbers, so its sum is 0, but "1,,2" has an empty piece and is an error.

Starterexercises/sum-numbers.rs
use std::error::Error;
/// Add up numbers written like "1, 2, 3". Return Err for a piece that is not a
/// number (including an empty piece, as in "1,,2") and for a sum that does not fit
/// in an i32. Text with only spaces has no numbers: its sum is 0.
fn sum_numbers(text: &str) -> Result<i32, Box<dyn Error>> {
todo!("split on commas, parse each piece with ?, add with checked_add")
}
fn main() {
assert_eq!(sum_numbers("1, 2, 3").unwrap(), 6);
assert_eq!(sum_numbers("-5, 5").unwrap(), 0);
assert_eq!(sum_numbers("").unwrap(), 0);
assert_eq!(sum_numbers(" ").unwrap(), 0);
assert!(sum_numbers("1,,2").is_err());
assert!(sum_numbers("1, 2,").is_err());
let message = sum_numbers("1, x").unwrap_err().to_string();
assert!(message.contains("invalid digit"), "{message}");
let message = sum_numbers("2147483647, 1").unwrap_err().to_string();
assert!(message.contains("does not fit"), "{message}");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/sum-numbers.solution.rsRun ↗
use std::error::Error;
/// Add up numbers written like "1, 2, 3". Return Err for a piece that is not a
/// number (including an empty piece, as in "1,,2") and for a sum that does not fit
/// in an i32. Text with only spaces has no numbers: its sum is 0.
fn sum_numbers(text: &str) -> Result<i32, Box<dyn Error>> {
if text.trim().is_empty() {
return Ok(0);
}
let mut total: i32 = 0;
for piece in text.split(',') {
let n: i32 = piece.trim().parse()?;
total = total
.checked_add(n)
.ok_or("the sum does not fit in an i32")?;
}
Ok(total)
}
fn main() {
assert_eq!(sum_numbers("1, 2, 3").unwrap(), 6);
assert_eq!(sum_numbers("-5, 5").unwrap(), 0);
assert_eq!(sum_numbers("").unwrap(), 0);
assert_eq!(sum_numbers(" ").unwrap(), 0);
assert!(sum_numbers("1,,2").is_err());
assert!(sum_numbers("1, 2,").is_err());
let message = sum_numbers("1, x").unwrap_err().to_string();
assert!(message.contains("invalid digit"), "{message}");
let message = sum_numbers("2147483647, 1").unwrap_err().to_string();
assert!(message.contains("does not fit"), "{message}");
println!("all checks passed");
}
$ cargo run
all checks passed

Read more