You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-06-26 10:41:01 +02:00
Comprehensive Rust v2 (#1073)
I've taken some work by @fw-immunant and others on the new organization of the course and condensed it into a form amenable to a text editor and some computational analysis. You can see the inputs in `course.py` but the interesting bits are the output: `outline.md` and `slides.md`. The idea is to break the course into more, smaller segments with exercises at the ends and breaks in between. So `outline.md` lists the segments, their duration, and sums those durations up per-day. It shows we're about an hour too long right now! There are more details of the segments in `slides.md`, or you can see mostly the same stuff in `course.py`. This now contains all of the content from the v1 course, ensuring both that we've covered everything and that we'll have somewhere to redirect every page. Fixes #1082. Fixes #1465. --------- Co-authored-by: Nicole LeGare <dlegare.1001@gmail.com> Co-authored-by: Martin Geisler <mgeisler@google.com>
This commit is contained in:
committed by
GitHub
parent
ea204774b6
commit
6d19292f16
13
src/error-handling/Cargo.toml
Normal file
13
src/error-handling/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "error-handling"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
thiserror = "*"
|
||||
anyhow = "*"
|
||||
|
||||
[[bin]]
|
||||
name = "parser"
|
||||
path = "exercise.rs"
|
@ -1,19 +0,0 @@
|
||||
# Converting Error Types
|
||||
|
||||
The effective expansion of `?` is a little more complicated than previously indicated:
|
||||
|
||||
```rust,ignore
|
||||
expression?
|
||||
```
|
||||
|
||||
works the same as
|
||||
|
||||
```rust,ignore
|
||||
match expression {
|
||||
Ok(value) => value,
|
||||
Err(err) => return Err(From::from(err)),
|
||||
}
|
||||
```
|
||||
|
||||
The `From::from` call here means we attempt to convert the error type to the
|
||||
type returned by the function.
|
@ -1,45 +0,0 @@
|
||||
# Deriving Error Enums
|
||||
|
||||
The [thiserror](https://docs.rs/thiserror/) crate is a popular way to create an
|
||||
error enum like we did on the previous page:
|
||||
|
||||
```rust,editable,compile_fail
|
||||
use std::{fs, io};
|
||||
use std::io::Read;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum ReadUsernameError {
|
||||
#[error("Could not read: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("Found no username in {0}")]
|
||||
EmptyUsername(String),
|
||||
}
|
||||
|
||||
fn read_username(path: &str) -> Result<String, ReadUsernameError> {
|
||||
let mut username = String::new();
|
||||
fs::File::open(path)?.read_to_string(&mut username)?;
|
||||
if username.is_empty() {
|
||||
return Err(ReadUsernameError::EmptyUsername(String::from(path)));
|
||||
}
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
//fs::write("config.dat", "").unwrap();
|
||||
match read_username("config.dat") {
|
||||
Ok(username) => println!("Username: {username}"),
|
||||
Err(err) => println!("Error: {err}"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
`thiserror`'s derive macro automatically implements `std::error::Error`, and optionally `Display`
|
||||
(if the `#[error(...)]` attributes are provided) and `From` (if the `#[from]` attribute is added).
|
||||
It also works for structs.
|
||||
|
||||
It doesn't affect your public API, which makes it good for libraries.
|
||||
|
||||
</details>
|
@ -1,41 +0,0 @@
|
||||
# Dynamic Error Types
|
||||
|
||||
Sometimes we want to allow any type of error to be returned without writing our own enum covering
|
||||
all the different possibilities. `std::error::Error` makes this easy.
|
||||
|
||||
```rust,editable,compile_fail
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use thiserror::Error;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Clone, Debug, Eq, Error, PartialEq)]
|
||||
#[error("Found no username in {0}")]
|
||||
struct EmptyUsernameError(String);
|
||||
|
||||
fn read_username(path: &str) -> Result<String, Box<dyn Error>> {
|
||||
let mut username = String::new();
|
||||
fs::File::open(path)?.read_to_string(&mut username)?;
|
||||
if username.is_empty() {
|
||||
return Err(EmptyUsernameError(String::from(path)).into());
|
||||
}
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
//fs::write("config.dat", "").unwrap();
|
||||
match read_username("config.dat") {
|
||||
Ok(username) => println!("Username: {username}"),
|
||||
Err(err) => println!("Error: {err}"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
This saves on code, but gives up the ability to cleanly handle different error cases differently in
|
||||
the program. As such it's generally not a good idea to use `Box<dyn Error>` in the public API of a
|
||||
library, but it can be a good option in a program where you just want to display the error message
|
||||
somewhere.
|
||||
|
||||
</details>
|
42
src/error-handling/error.md
Normal file
42
src/error-handling/error.md
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
# Dynamic Error Types
|
||||
|
||||
Sometimes we want to allow any type of error to be returned without writing our
|
||||
own enum covering all the different possibilities. The `std::error::Error`
|
||||
trait makes it easy to create a trait object that can contain any error.
|
||||
|
||||
```rust,editable
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
|
||||
fn read_count(path: &str) -> Result<i32, Box<dyn Error>> {
|
||||
let mut count_str = String::new();
|
||||
fs::File::open(path)?.read_to_string(&mut count_str)?;
|
||||
let count: i32 = count_str.parse()?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fs::write("count.dat", "1i3").unwrap();
|
||||
match read_count("count.dat") {
|
||||
Ok(count) => println!("Count: {count}"),
|
||||
Err(err) => println!("Error: {err}"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
The `read_count` function can return `std::io::Error` (from file operations) or
|
||||
`std::num::ParseIntError` (from `String::parse`).
|
||||
|
||||
Boxing errors saves on code, but gives up the ability to cleanly handle different error cases differently in
|
||||
the program. As such it's generally not a good idea to use `Box<dyn Error>` in the public API of a
|
||||
library, but it can be a good option in a program where you just want to display the error message
|
||||
somewhere.
|
||||
|
||||
</details>
|
20
src/error-handling/exercise.md
Normal file
20
src/error-handling/exercise.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
minutes: 20
|
||||
---
|
||||
|
||||
# Exercise: Rewriting with Result
|
||||
|
||||
The following implements a very simple parser for an expression language.
|
||||
However, it handles errors by panicking. Rewrite it to instead use idiomatic
|
||||
error handling and propagate errors to a return from `main`. Feel free to use
|
||||
`thiserror` and `anyhow`.
|
||||
|
||||
HINT: start by fixing error handling in the `parse` function. Once that is
|
||||
working correctly, update `Tokenizer` to implement
|
||||
`Iterator<Item=Result<Token, TokenizerError>>` and handle that in the parser.
|
||||
|
||||
```rust,editable
|
||||
{{#include exercise.rs:types}}
|
||||
|
||||
{{#include exercise.rs:panics}}
|
||||
```
|
211
src/error-handling/exercise.rs
Normal file
211
src/error-handling/exercise.rs
Normal file
@ -0,0 +1,211 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// ANCHOR: solution
|
||||
use thiserror::Error;
|
||||
// ANCHOR: types
|
||||
use std::iter::Peekable;
|
||||
use std::str::Chars;
|
||||
|
||||
/// An arithmetic operator.
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum Op {
|
||||
Add,
|
||||
Sub,
|
||||
}
|
||||
|
||||
/// A token in the expression language.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Token {
|
||||
Number(String),
|
||||
Identifier(String),
|
||||
Operator(Op),
|
||||
}
|
||||
|
||||
/// An expression in the expression language.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Expression {
|
||||
/// A reference to a variable.
|
||||
Var(String),
|
||||
/// A literal number.
|
||||
Number(u32),
|
||||
/// A binary operation.
|
||||
Operation(Box<Expression>, Op, Box<Expression>),
|
||||
}
|
||||
// ANCHOR_END: types
|
||||
|
||||
fn tokenize(input: &str) -> Tokenizer {
|
||||
return Tokenizer(input.chars().peekable());
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum TokenizerError {
|
||||
#[error("Unexpected character '{0}' in input")]
|
||||
UnexpectedCharacter(char),
|
||||
}
|
||||
|
||||
struct Tokenizer<'a>(Peekable<Chars<'a>>);
|
||||
|
||||
impl<'a> Iterator for Tokenizer<'a> {
|
||||
type Item = Result<Token, TokenizerError>;
|
||||
|
||||
fn next(&mut self) -> Option<Result<Token, TokenizerError>> {
|
||||
let Some(c) = self.0.next() else {
|
||||
return None;
|
||||
};
|
||||
match c {
|
||||
'0'..='9' => {
|
||||
let mut num = String::from(c);
|
||||
while let Some(c @ '0'..='9') = self.0.peek() {
|
||||
num.push(*c);
|
||||
self.0.next();
|
||||
}
|
||||
Some(Ok(Token::Number(num)))
|
||||
}
|
||||
'a'..='z' => {
|
||||
let mut ident = String::from(c);
|
||||
while let Some(c @ 'a'..='z' | c @ '_' | c @ '0'..='9') = self.0.peek() {
|
||||
ident.push(*c);
|
||||
self.0.next();
|
||||
}
|
||||
Some(Ok(Token::Identifier(ident)))
|
||||
}
|
||||
'+' => Some(Ok(Token::Operator(Op::Add))),
|
||||
'-' => Some(Ok(Token::Operator(Op::Sub))),
|
||||
_ => Some(Err(TokenizerError::UnexpectedCharacter(c))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum ParserError {
|
||||
#[error("Tokenizer error: {0}")]
|
||||
TokenizerError(#[from] TokenizerError),
|
||||
#[error("Unexpected end of input")]
|
||||
UnexpectedEOF,
|
||||
#[error("Unexpected token {0:?}")]
|
||||
UnexpectedToken(Token),
|
||||
#[error("Invalid number")]
|
||||
InvalidNumber(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
fn parse(input: &str) -> Result<Expression, ParserError> {
|
||||
let mut tokens = tokenize(input);
|
||||
|
||||
fn parse_expr<'a>(tokens: &mut Tokenizer<'a>) -> Result<Expression, ParserError> {
|
||||
let Some(tok) = tokens.next().transpose()? else {
|
||||
return Err(ParserError::UnexpectedEOF);
|
||||
};
|
||||
let expr = match tok {
|
||||
Token::Number(num) => {
|
||||
let v = num.parse()?;
|
||||
Expression::Number(v)
|
||||
}
|
||||
Token::Identifier(ident) => Expression::Var(ident),
|
||||
Token::Operator(_) => return Err(ParserError::UnexpectedToken(tok)),
|
||||
};
|
||||
// Look ahead to parse a binary operation if present.
|
||||
Ok(match tokens.next() {
|
||||
None => expr,
|
||||
Some(Ok(Token::Operator(op))) => {
|
||||
Expression::Operation(Box::new(expr), op, Box::new(parse_expr(tokens)?))
|
||||
}
|
||||
Some(Err(e)) => return Err(e.into()),
|
||||
Some(Ok(tok)) => return Err(ParserError::UnexpectedToken(tok)),
|
||||
})
|
||||
}
|
||||
|
||||
parse_expr(&mut tokens)
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let expr = parse("10+foo+20-30")?;
|
||||
println!("{expr:?}");
|
||||
Ok(())
|
||||
}
|
||||
// ANCHOR_END: solution
|
||||
|
||||
/*
|
||||
// ANCHOR: panics
|
||||
fn tokenize(input: &str) -> Tokenizer {
|
||||
return Tokenizer(input.chars().peekable());
|
||||
}
|
||||
|
||||
struct Tokenizer<'a>(Peekable<Chars<'a>>);
|
||||
|
||||
impl<'a> Iterator for Tokenizer<'a> {
|
||||
type Item = Token;
|
||||
|
||||
fn next(&mut self) -> Option<Token> {
|
||||
let Some(c) = self.0.next() else {
|
||||
return None;
|
||||
};
|
||||
match c {
|
||||
'0'..='9' => {
|
||||
let mut num = String::from(c);
|
||||
while let Some(c @ '0'..='9') = self.0.peek() {
|
||||
num.push(*c);
|
||||
self.0.next();
|
||||
}
|
||||
Some(Token::Number(num))
|
||||
}
|
||||
'a'..='z' => {
|
||||
let mut ident = String::from(c);
|
||||
while let Some(c @ 'a'..='z' | c @ '_' | c @ '0'..='9') = self.0.peek() {
|
||||
ident.push(*c);
|
||||
self.0.next();
|
||||
}
|
||||
Some(Token::Identifier(ident))
|
||||
}
|
||||
'+' => Some(Token::Operator(Op::Add)),
|
||||
'-' => Some(Token::Operator(Op::Sub)),
|
||||
_ => panic!("Unexpected character {c}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(input: &str) -> Expression {
|
||||
let mut tokens = tokenize(input);
|
||||
|
||||
fn parse_expr<'a>(tokens: &mut Tokenizer<'a>) -> Expression {
|
||||
let Some(tok) = tokens.next() else {
|
||||
panic!("Unexpected end of input");
|
||||
};
|
||||
let expr = match tok {
|
||||
Token::Number(num) => {
|
||||
let v = num.parse().expect("Invalid 32-bit integer'");
|
||||
Expression::Number(v)
|
||||
}
|
||||
Token::Identifier(ident) => Expression::Var(ident),
|
||||
Token::Operator(_) => panic!("Unexpected token {tok:?}"),
|
||||
};
|
||||
// Look ahead to parse a binary operation if present.
|
||||
match tokens.next() {
|
||||
None => expr,
|
||||
Some(Token::Operator(op)) => {
|
||||
Expression::Operation(Box::new(expr), op, Box::new(parse_expr(tokens)))
|
||||
}
|
||||
Some(tok) => panic!("Unexpected token {tok:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
parse_expr(&mut tokens)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let expr = parse("10+foo+20-30");
|
||||
println!("{expr:?}");
|
||||
}
|
||||
// ANCHOR_END: panics
|
||||
*/
|
@ -1,23 +0,0 @@
|
||||
# Catching the Stack Unwinding
|
||||
|
||||
By default, a panic will cause the stack to unwind. The unwinding can be caught:
|
||||
|
||||
```rust,editable
|
||||
use std::panic;
|
||||
|
||||
fn main() {
|
||||
let result = panic::catch_unwind(|| {
|
||||
"No problem here!"
|
||||
});
|
||||
println!("{result:?}");
|
||||
|
||||
let result = panic::catch_unwind(|| {
|
||||
panic!("oh no!");
|
||||
});
|
||||
println!("{result:?}");
|
||||
}
|
||||
```
|
||||
|
||||
- This can be useful in servers which should keep running even if a single
|
||||
request crashes.
|
||||
- This does not work if `panic = 'abort'` is set in your `Cargo.toml`.
|
@ -1,5 +1,11 @@
|
||||
---
|
||||
minutes: 3
|
||||
---
|
||||
|
||||
# Panics
|
||||
|
||||
Rust handles fatal errors with a "panic".
|
||||
|
||||
Rust will trigger a panic if a fatal error happens at runtime:
|
||||
|
||||
```rust,editable,should_panic
|
||||
@ -11,4 +17,35 @@ fn main() {
|
||||
|
||||
* Panics are for unrecoverable and unexpected errors.
|
||||
* Panics are symptoms of bugs in the program.
|
||||
* Runtime failures like failed bounds checks can panic
|
||||
* Assertions (such as `assert!`) panic on failure
|
||||
* Purpose-specific panics can use the `panic!` macro.
|
||||
* A panic will "unwind" the stack, dropping values just as if the functions had returned.
|
||||
* Use non-panicking APIs (such as `Vec::get`) if crashing is not acceptable.
|
||||
|
||||
<details>
|
||||
|
||||
By default, a panic will cause the stack to unwind. The unwinding can be caught:
|
||||
|
||||
```rust,editable
|
||||
use std::panic;
|
||||
|
||||
fn main() {
|
||||
let result = panic::catch_unwind(|| {
|
||||
"No problem here!"
|
||||
});
|
||||
println!("{result:?}");
|
||||
|
||||
let result = panic::catch_unwind(|| {
|
||||
panic!("oh no!");
|
||||
});
|
||||
println!("{result:?}");
|
||||
}
|
||||
```
|
||||
|
||||
- Catching is unusual; do not attempt to implement exceptions with `catch_unwind`!
|
||||
- This can be useful in servers which should keep running even if a single
|
||||
request crashes.
|
||||
- This does not work if `panic = 'abort'` is set in your `Cargo.toml`.
|
||||
|
||||
</details>
|
||||
|
@ -1,33 +0,0 @@
|
||||
# Structured Error Handling with `Result`
|
||||
|
||||
We have already seen the `Result` enum. This is used pervasively when errors are
|
||||
expected as part of normal operation:
|
||||
|
||||
```rust,editable
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
|
||||
fn main() {
|
||||
let file = fs::File::open("diary.txt");
|
||||
match file {
|
||||
Ok(mut file) => {
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents);
|
||||
println!("Dear diary: {contents}");
|
||||
},
|
||||
Err(err) => {
|
||||
println!("The diary could not be opened: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
* As with `Option`, the successful value sits inside of `Result`, forcing the developer to
|
||||
explicitly extract it. This encourages error checking. In the case where an error should never happen,
|
||||
`unwrap()` or `expect()` can be called, and this is a signal of the developer intent too.
|
||||
* `Result` documentation is a recommended read. Not during the course, but it is worth mentioning.
|
||||
It contains a lot of convenience methods and functions that help functional-style programming.
|
||||
|
||||
</details>
|
6
src/error-handling/solution.md
Normal file
6
src/error-handling/solution.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Solution
|
||||
|
||||
<!-- compile_fail because `mdbook test` does not allow use of `thiserror` -->
|
||||
```rust,editable,compile_fail
|
||||
{{#include exercise.rs:solution}}
|
||||
```
|
@ -1,14 +1,23 @@
|
||||
# Adding Context to Errors
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
The widely used [anyhow](https://docs.rs/anyhow/) crate can help you add
|
||||
contextual information to your errors and allows you to have fewer
|
||||
custom error types:
|
||||
# `thiserror` and `anyhow`
|
||||
|
||||
The [`thiserror`](https://docs.rs/thiserror/) and [`anyhow`](https://docs.rs/anyhow/)
|
||||
crates are widley used to simplify error handling. `thiserror` helps
|
||||
create custom error types that implement `From<T>`. `anyhow` helps with error
|
||||
handling in functions, including adding contextual information to your errors.
|
||||
|
||||
```rust,editable,compile_fail
|
||||
use std::{fs, io};
|
||||
use std::io::Read;
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Error, PartialEq)]
|
||||
#[error("Found no username in {0}")]
|
||||
struct EmptyUsernameError(String);
|
||||
|
||||
fn read_username(path: &str) -> Result<String> {
|
||||
let mut username = String::with_capacity(100);
|
||||
fs::File::open(path)
|
||||
@ -16,7 +25,7 @@ fn read_username(path: &str) -> Result<String> {
|
||||
.read_to_string(&mut username)
|
||||
.context("Failed to read")?;
|
||||
if username.is_empty() {
|
||||
bail!("Found no username in {path}");
|
||||
bail!(EmptyUsernameError(path));
|
||||
}
|
||||
Ok(username)
|
||||
}
|
||||
@ -32,6 +41,8 @@ fn main() {
|
||||
|
||||
<details>
|
||||
|
||||
* The `Error` derive macro is provided by `thiserror`, and has lots of useful
|
||||
attributes like `#[error]` to help define a useful error type.
|
||||
* `anyhow::Result<V>` is a type alias for `Result<V, anyhow::Error>`.
|
||||
* `anyhow::Error` is essentially a wrapper around `Box<dyn Error>`. As such it's again generally not
|
||||
a good choice for the public API of a library, but is widely used in applications.
|
@ -1,4 +1,29 @@
|
||||
# Converting Error Types
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
# Try Conversions
|
||||
|
||||
The effective expansion of `?` is a little more complicated than previously indicated:
|
||||
|
||||
```rust,ignore
|
||||
expression?
|
||||
```
|
||||
|
||||
works the same as
|
||||
|
||||
```rust,ignore
|
||||
match expression {
|
||||
Ok(value) => value,
|
||||
Err(err) => return Err(From::from(err)),
|
||||
}
|
||||
```
|
||||
|
||||
The `From::from` call here means we attempt to convert the error type to the
|
||||
type returned by the function. This makes it easy to encapsulate errors into
|
||||
higher-level errors.
|
||||
|
||||
## Example
|
||||
|
||||
```rust,editable
|
||||
use std::error::Error;
|
||||
@ -47,10 +72,15 @@ fn main() {
|
||||
|
||||
<details>
|
||||
|
||||
Key points:
|
||||
The return type of the function has to be compatible with the nested functions it calls. For instance,
|
||||
a function returning a `Result<T, Err>` can only apply the `?` operator on a function returning a
|
||||
`Result<AnyT, Err>`. It cannot apply the `?` operator on a function returning an `Option<AnyT>` or `Result<T, OtherErr>`
|
||||
unless `OtherErr` implements `From<Err>`. Reciprocally, a function returning an `Option<T>` can only apply the `?` operator
|
||||
on a function returning an `Option<AnyT>`.
|
||||
|
||||
You can convert incompatible types into one another with the different `Option` and `Result` methods
|
||||
such as `Option::ok_or`, `Result::ok`, `Result::err`.
|
||||
|
||||
* The `username` variable can be either `Ok(string)` or `Err(error)`.
|
||||
* Use the `fs::write` call to test out the different scenarios: no file, empty file, file with username.
|
||||
|
||||
It is good practice for all error types that don't need to be `no_std` to implement `std::error::Error`, which requires `Debug` and `Display`. The `Error` crate for `core` is only available in [nightly](https://github.com/rust-lang/rust/issues/103765), so not fully `no_std` compatible yet.
|
||||
|
||||
@ -58,4 +88,6 @@ It's generally helpful for them to implement `Clone` and `Eq` too where possible
|
||||
life easier for tests and consumers of your library. In this case we can't easily do so, because
|
||||
`io::Error` doesn't implement them.
|
||||
|
||||
A common alternative to a `From` implementation is `Result::map_err`, especially when the conversion only happens in one place.
|
||||
|
||||
</details>
|
@ -1,7 +1,13 @@
|
||||
# Propagating Errors with `?`
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
The try-operator `?` is used to return errors to the caller. It lets you turn
|
||||
the common
|
||||
# Try Operator
|
||||
|
||||
Runtime errors like connection-refused or file-not-found are handled with the
|
||||
`Result` type, but matching this type on every call can be cumbersome. The
|
||||
try-operator `?` is used to return errors to the caller. It lets you turn the
|
||||
common
|
||||
|
||||
```rust,ignore
|
||||
match some_expression {
|
||||
@ -45,16 +51,12 @@ fn main() {
|
||||
|
||||
<details>
|
||||
|
||||
Simplify the `read_username` function to use `?`.
|
||||
|
||||
Key points:
|
||||
|
||||
* The `username` variable can be either `Ok(string)` or `Err(error)`.
|
||||
* Use the `fs::write` call to test out the different scenarios: no file, empty file, file with username.
|
||||
* The return type of the function has to be compatible with the nested functions it calls. For instance,
|
||||
a function returning a `Result<T, Err>` can only apply the `?` operator on a function returning a
|
||||
`Result<AnyT, Err>`. It cannot apply the `?` operator on a function returning an `Option<AnyT>` or `Result<T, OtherErr>`
|
||||
unless `OtherErr` implements `From<Err>`. Reciprocally, a function returning an `Option<T>` can only apply the `?` operator
|
||||
on a function returning an `Option<AnyT>`.
|
||||
* You can convert incompatible types into one another with the different `Option` and `Result` methods
|
||||
such as `Option::ok_or`, `Result::ok`, `Result::err`.
|
||||
* Note that `main` can return a `Result<(), E>` as long as it implements `std::process:Termination`. In practice, this means that `E` implements `Debug`. The executable will print the `Err` variant and return a nonzero exit status on error.
|
||||
|
||||
</details>
|
Reference in New Issue
Block a user