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
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}"), }}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) } }}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}") } }}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}")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}`); }}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()); } } }}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 callerstatic uint ParseAge(string s) => uint.Parse(s);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 pointerconst 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;}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"; } }}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:Resultfor aResult,Optionfor anOption. Inmain, write-> Result<(), Box<dyn Error>>to use it.- In real projects, the
thiserrorandanyhowcrates (add-on libraries) remove most of the boilerplate: see Errors in real apps.
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.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)}43
fun main() { val n: Int = "42".toInt() // throws if the text isn't a number println(n + 1)}43
n = int("42")print(n + 1)43
const n: number = parseInt("42");console.log(n + 1);43
public class Main { public static void main(String[] args) { int n = Integer.parseInt("42"); System.out.println(n + 1); }}43
var n = uint.Parse("42");Console.WriteLine(n + 1);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;}43 1
#include <iostream>#include <string>
int main() { int n = std::stoi("42"); std::cout << n + 1 << "\n";}43
What does the compiler say? Guess first, then open.
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"); | +++++++++++++++++
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.
Kotlin runs this and prints 43. With "abc", toInt() would throw a NumberFormatException, and nothing in the code warns you.
Python prints 43. int("42") works, and if the text were "abc" it would raise a ValueError you didn't catch.
Prints 43. parseInt never throws: bad input like "abc" gives NaN, and the math carries on with it.
This prints 43. NumberFormatException is unchecked, so nothing makes you handle it; with "abc" the same line would throw at runtime.
uint.Parse hands back the number directly and prints 43. A bad string would throw a FormatException, which nothing here catches.
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.
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.
parse returns Result<u32, ...>, not a u32. The compiler does not look at the text "42". It only knows that parsing text can fail.
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.
fn main() { let n: u32 = "42".parse().unwrap_or(0); // 0 if it fails, chosen on purpose println!("{}", n + 1);}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:
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}"), } }}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 typeparse 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 ?:
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", ""));}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:
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)); }}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:
// 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"));}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
Fromimpl 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:
fn main() { let n: u32 = "42".parse()?; println!("n: {n}");}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:
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"));}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:
// 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");}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:
fn main() { println!("before"); let age: u32 = "forty".parse().unwrap(); println!("age: {age}");}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 backtraceRead 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):
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");}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:
// 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(&[]));}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:
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}");}
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 backtraceBoth are fine in some places:
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}");}home: 127.0.0.1 width: 80
- A value you can check by reading the code, like the hard-coded address. Use
expectand 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”.
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));}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 anOptioninto aResult:NonebecomesErr(error).ok_or_else(|| …)builds the error only when needed, which matters when that costs something, such as making aString.map_err(f)rewrites the error and leaves anOkalone. 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 aResultitself, 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 |
// 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.
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)); }}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:
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}"); } } } }}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 theDebug:lines, the shape of the value for programmers.impl fmt::Display for ConfigErrorwrites the message a person reads, theDisplay:lines (what{}prints). Animplblock holds the functions of a type, as in Methods. This one readsimpl Trait for Type: a trait is a named set of functions a type promises to provide, much like an interface, andDisplayis the one{}asks for (Traits covers them). For now read it as “here is how to print aConfigError”.write!isprintln!that writes intof.fmt::Resultis a short name forResult<(), fmt::Error>.impl Error for ConfigErrormarks the type as an error, whichBox<dyn Error>and other tools need. It requiresDebugandDisplay. Itssourcemethod is optional: it returns the error that caused this one, orNone. Copy that signature as written. A message describes its own level and leaves the cause tosource, so the cause is not printed twice.impl From<ParseIntError> for ConfigErroris the function?calls. Because it exists,text.parse()?compiles in a function that returnsResult<_, ConfigError>, andNotANumberis built for you. When two failures of one type need different context, usemap_errinstead.
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):
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})"); } } }}"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:
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(())}port: 8080
When main returns an Err, the program prints Error: and the error’s Debug form, then exits with a failure code:
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> { println!("starting"); let port: u16 = "http".parse()?; println!("port: {port}"); Ok(())}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:
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(())}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
Coming from Python or TypeScript
Coming from Python
| You write in Python | In Rust |
|---|---|
raise ConfigError(…) |
return Err(ConfigError::…) |
class ConfigError(Exception) and subclasses |
one enum, a variant per case |
try: … except ValueError as e:, isinstance(e, X) |
match f() { Ok(v) => …, Err(e) => … }, with a pattern per variant |
| letting an exception fly up to the caller | ? |
str(e), repr(e) |
Display ({e}), Debug ({e:?}) |
raise X from e, e.__cause__ |
map_err into a variant that keeps the cause, and source() |
with, finally |
Drop, run at the end of scope (see Cleanup still happens) |
Python’s style is EAFP, “easier to ask forgiveness than permission”: try it and handle what goes wrong. Rust works the same way in spirit. The Python version of custom-error.rs needs only a class and raise ... from:
class ConfigError(Exception): pass
def threads(data): try: return int(data["threads"]) except KeyError as e: raise ConfigError("the threads setting is missing") from e except ValueError as e: raise ConfigError("the threads setting is not a number") from e
for data in [{"threads": "8"}, {}, {"threads": "many"}]: try: print("threads:", threads(data)) except ConfigError as e: print("error:", e, "| cause:", repr(e.__cause__))threads: 8
error: the threads setting is missing | cause: KeyError('threads')
error: the threads setting is not a number | cause: ValueError("invalid literal for int() with base 10: 'many'")Nothing in def threads(data) tells a caller that it raises ConfigError. In Rust the signature says Result<u32, ConfigError>. An uncaught Python exception ends the program with a traceback:
def parse_age(s): return int(s)
def load(): return parse_age("abc")
print(load())Traceback (most recent call last):
File "main.py", line 9, in <module>
print(load())
~~~~^^
File "main.py", line 6, in load
return parse_age("abc")
File "main.py", line 2, in parse_age
return int(s)
ValueError: invalid literal for int() with base 10: 'abc'A Rust Err that reaches main prints one line and no trace, even with RUST_BACKTRACE=1. A panic prints the place it happened, and RUST_BACKTRACE=1 adds the call chain.
Coming from TypeScript
| You write in TypeScript | In Rust |
|---|---|
throw new Error(…) |
return Err(MyError::…) |
try { } catch (e) { } |
match f() { Ok(v) => …, Err(e) => … } |
| letting an exception fly up | ? |
new Error(msg, { cause: e }), e.cause |
map_err into a variant that keeps the cause, and source() |
finally |
Drop, run at the end of scope (see Cleanup still happens) |
In strict TypeScript a catch variable is unknown, so you cannot read e.message until you check what it is (for example with e instanceof Error):
try { JSON.parse("{");} catch (e) { console.log(e.message);}main.ts(4,15): error TS18046: 'e' is of type 'unknown'.
You can write a Result yourself as a union, and the compiler narrows it as it does for Rust’s match:
// A Result you write yourself: a union the compiler narrows.type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function parseAge(s: string): Result<number, string> { if (!/^\d+$/.test(s)) return { ok: false, error: `not a number: ${s}` }; return { ok: true, value: Number(s) };}
for (const text of ["36", "abc"]) { const r = parseAge(text); if (r.ok) { console.log("ok", r.value); } else { console.log("error", r.error); // r.value does not exist on this branch }}ok 36 error not a number: abc
It is the same idea, but a convention you write and follow yourself, and any library can still throw. In Rust there is one Result that everything uses, and ? is built in.
What Rust gives up. A raise can come from anywhere. In Rust, making a deep function fallible changes the signatures above it (or needs a Box<dyn Error>), every fallible call carries a ?, and you write the error types: custom-error.rs needs an enum and four impl blocks where Python needs a three-line class. A returned Err carries no traceback.
What Rust gains. No uncaught KeyError three layers down: the signature tells you. And unwrap is the honest name for what your code does today when it does not catch: it ends the program.
Coming from C or C++
Coming from C
C reports failure through return codes, and often through the errno variable as well. Everything depends on the caller checking:
#include <errno.h>#include <stdio.h>#include <stdlib.h>
// strtol reports trouble three ways: errno, the end pointer, and the value.// A caller has to check all of them, and nothing forces it to.int parse(const char *s, long *out) { char *end; errno = 0; long n = strtol(s, &end, 10); if (errno == ERANGE) return -1; // too big if (end == s || *end != '\0') return -2; // no digits, or junk after them *out = n; return 0;}
int main(void) { const char *inputs[] = {"42", "abc", "", "99999999999999999999"}; for (int i = 0; i < 4; i++) { long n = 0; int code = parse(inputs[i], &n); printf("parse(\"%s\"): code %d, value %ld\n", inputs[i], code, n); } return 0;}parse("42"): code 0, value 42
parse("abc"): code -2, value 0
parse(""): code -2, value 0
parse("99999999999999999999"): code -1, value 0strtol uses three signals (the return value, errno, and the end pointer), and nothing makes the caller look at any of them. A Result<T, E> holds the same information in one value that Rust will not let you read unchecked.
Coming from C++
C++ has two styles: exceptions and returned codes. Nothing in this signature says the function can throw:
#include <iostream>#include <string>
// Nothing in this signature says it can throw.int parse_age(const std::string& s) { return std::stoi(s);}
int main() { std::cout << parse_age("abc") << "\n";}terminate called after throwing an instance of 'std::invalid_argument' what(): stoi
A returned code is easy to ignore. [[nodiscard]] makes GCC warn (see below the code): on a function it covers that function, and on a type it covers every function returning it, which is what Result has built in:
#include <iostream>
// On a function: only this function warns.[[nodiscard]] int load(int id) { return id > 0 ? 0 : -1;}
// On a type: every function that returns a Status warns.struct [[nodiscard]] Status { int code;};
Status save(int id) { return {id > 0 ? 0 : -1};}
int main() { load(-5); // the failure code is thrown away save(-5); // so is this one std::cout << "done\n";}main.cpp: In function ‘int main()’:
main.cpp:18:9: warning: ignoring return value of ‘int load(int)’, declared with attribute ‘nodiscard’ [-Wunused-result]
18 | load(-5); // the failure code is thrown away
| ~~~~^~~~
main.cpp:4:19: note: declared here
4 | [[nodiscard]] int load(int id) {
| ^~~~
main.cpp:19:9: warning: ignoring returned value of type ‘Status’, declared with attribute ‘nodiscard’ [-Wunused-result]
19 | save(-5); // so is this one
| ~~~~^~~~
main.cpp:13:8: note: in call to ‘Status save(int)’, declared here
13 | Status save(int id) {
| ^~~~
main.cpp:9:22: note: ‘Status’ declared here
9 | struct [[nodiscard]] Status {
| ^~~~~~done
C++23 adds std::expected<T, E>, the same idea as Result<T, E>. It has and_then, transform and or_else like Result, but no ?: an early return still needs if (!r) return std::unexpected(r.error()); on every call.
| You write | In Rust |
|---|---|
int f(…, T *out) returning 0 or an error code |
fn f(…) -> Result<T, E> |
if (err) return err; after each call |
? |
throw std::runtime_error(…), what() |
return Err(…) (a panic for a bug), printed with Display ({e}) |
try { … } catch (const std::exception& e) |
match, or ? to pass it up |
std::expected<T, E> (C++23) |
Result<T, E> |
| a destructor or RAII guard | Drop, run at the end of scope, including on a ? return (see Cleanup still happens) |
What Rust gives up. A constructor cannot return a Result, so a type that can fail to build uses a function such as fn new(…) -> Result<Self, E>. Each fallible call carries a ?. Panics unwind and clean up (see Panics above), but they are for bugs, not for control flow.
What Rust gains. Every early exit is written as ? at the call site, and an ignored Result draws a warning without any attribute on your functions.
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:
A Box<dyn Error> is two pointers wide, because it must say both where the error is and what type it is:
{e} works without knowing the type. size_of below says 16 bytes: two 8-byte pointers on this 64-bit platform.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) );}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.
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));}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.
/// 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
/// 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");}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.
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
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");}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.
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
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");}all checks passed
Read more
Resultin the standard library. Every method, each with an example.std::error::Error, includingsource.std::process::exitandExitCode.- The Rust Book, chapter 9: Unrecoverable errors with
panic!, Recoverable errors withResultand To panic or not to panic. - Rust by Example: Error handling.
- Errors in real apps:
thiserrorandanyhow.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.