Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Idiomatic Rust code guidance based on Apollo GraphQL's best practices handbook for ownership, errors, and performance.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/chapter_04.md
1# Chapter 4 - Errors Handling23Rust enforces a strict error handling approach, but *how* you handle them defines where your code feels ergonomic, consistent and safe - as opposing cryptic and painful. This chapter dives into best practices for modeling and managing fallible operations across libraries and binaries.45> Even if you decide to crash you application with `unwrap` or `expect`, Rust forces you to declare that intentionally.67## 4.1 Prefer `Result`, avoid panic 🫨89Rust has a powerful type that wraps fallible data, [`Result<T, E>`](https://doc.rust-lang.org/std/result/), this allows us to handle Error cases according to our needs and manage the state of the application based on that.1011* If your function can fail, prefer to return a `Result`:12```rust13fn divide(x: f64, y: f64) -> Result<f64, DivisionError> {14if y == 0.0 {15Err(DivisionError::DividedByZero)16} else {17Ok(x / y)18}19}20```2122* Use `panic!` only in unrecoverable conditions - typically tests, assertions, bugs or a need to crash the application for some explicit reason.23* There are 3 relevant macros that can replace `panic!` in appropriate conditions:24* `todo!`, similar to panic, but alerts the compiler that you are aware that there is code missing.25* `unreachable!`, you have reasoned about the code block and are sure that condition `xyz` is not possible and if ever becomes possible you want to be alerted.26* `unimplemented!`, specially useful for alerting that a block is not yet implement with a reason.2728## 4.2 Avoid `unwrap`/`expect` in Production2930Although `expect` is preferred to `unwrap`, as it can have context, they should be avoided in production code as there are smarter alternatives to them. Considering that, they should be used in the following scenarios:31- In tests, assertions or test helper functions.32- When failure is impossible.33- When the smarter options can't handle the specific case.3435### 🚨 Alternative ways of handling `unwrap`/`expect`:3637* If your `Result` (or `Option`) can have a predefined early return value in case of `Result::Err`, that doesn't need to know the `Err` value, use `let Ok(..) = else { return ... }` pattern, as it helps with flatten functions:38```rust39let Ok(json) = serde_json::from_str(&input) else {40return Err(MyError::InvalidJson);41}42```43* If your `Result` (or `Option`) needs error recovery in case of `Result::Err`, that doesn't need to know the `Err` value, use `if let Ok(..) else { ... }` pattern:44```rust45if let Ok(json) = serde_json::from_str(&input) else {46...47} else {48Err(do_something_with_input(&input))49}50```51* Functions that can have to handle `Option::None` values are recommended to return `Result<T, E>`, where `E` is a crate or module level error, like the examples above.52* Lastly `unwrap_or`, `unwrap_or_else` or `unwrap_or_default`, these functions help you create alternative exits to unwrap that manage the uninitialized values.5354## 4.3 `thiserror` for Crate level errors5556Deriving Error manually is verbose and error prone, the rust ecosystem has a really good crate to help with this, `thiserror`. It allows you to create error types that easily implement `From` trait as well as easy error message (`Display`), improving developer experience while working seamlessly with `?` and integrating with `std::error::Error`:5758```rust59#[derive(Debug, thiserror::Error)]60pub enum MyError {61#[error("Network Timeout")]62Timeout,63#[error("Invalid data: {0}")]64InvalidData(String),65#[error(transparent)]66Serialization(#[from] serde_json::Error),67#[error("Invalid request information. Header: {headers}, Metadata: {metadata}")]68InvalidRequest {69headers: Headers,70metadata: Metadata71}72}73```7475### Error Hierarchies and Wrapping7677For layered systems the best practice is to use nested `enum/struct` errors with `#[from]`:7879```rust80use crate::database::DbError;81use crate::external_services::ExternalHttpError;8283#[derive(Debug, thiserror::Error)]84pub enum ServiceError {85#[error("Database handler error: {0}")]86Db(#[from] DbError),87#[error("External services error: {0}")]88ExternalServices(#[from] ExternalHttpError)89}90```9192## 4.4 Reserve `anyhow` for Binaries9394`anyhow` is an amazing crate, and quite useful for projects that are beginning and need accelerated speed. However, there is a turning point where it just painfully propagates through your code, considering this, `anyhow` is recommended only for **binaries**, where ergonomic error handling is needed and there is no need for precise error types:9596```rust97use anyhow::{Context, Result, anyhow};9899fn main() -> Result<()> {100let content = std::fs::read_to_string("config.json")101.context("Failed to read config file")?;102Config::from_str(&content)103.map_err(|err| anyhow!("Config parsing error: {err}"))104}105```106107### 🚨 `Anyhow` Gotchas108109* Keeping the `context` and `anyhow` strings up-to-date in all code base is harder than keeping `thiserror` messages as you don't have a single point of entry.110* `anyhow::Result` erases context that a caller might need, so avoid using it in a library.111* test helper functions can use `anyhow` with little to no issues.112113## 4.5 Use `?` to Bubble Errors114115Prefer using `?` over verbose alternatives like `match` chains:116```rust117fn handle_request(req: &Request) -> Result<ValidatedRequest, MyError> {118validate_headers(req)?;119validate_body_format(req)?;120validate_credentials(req)?;121let body = Body::try_from(req)?;122123Ok(ValidatedRequest::try_from((req, body))?)124}125```126127> In case error recovery is needed, use `or_else`, `map_err`, `if let Ok(..) else`. To **inspect or log your error**, use `inspect_err`.128129## 4.6 Unit Test should exercise errors130131While many errors don't implement PartialEq and Eq, making it hard to do direct assertions between them, it is possible to check the error messages with `format!` or `to_string()`, making the errors meaningful and test validated:132133```rust134#[test]135fn error_does_not_implement_partial_eq() {136let err = divide(10., 0.0).unwrap_err();137assert_eq!(err.to_string(), "division by zero");138}139140#[test]141fn error_implements_partial_eq() {142let err = process(my_value).unwrap_err();143144assert_eq!(145err,146MyError {147..148}149)150}151```152153## 4.7 Important Topics154155### Custom Error Structs156157Sometimes you don't need an enum to handle your errors, as there is only one type of error that your module can have. This can be solved with `struct Errors`:158159```rust160#[derive(Debug, thiserror::Error, PartialEq)]161#[error("Request failed with code `{code}`: {message}")]162struct HttpError {163code: u16,164message: String165}166```167168### Async Errors169170When using async runtimes, like Tokio, make sure that your errors implement `Send + Sync + 'static` where needed, specially in tasks or across `.await` boundaries:171172```rust173#[tokio::main]174async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {175...176Ok(())177}178```179180> Avoid `Box<dyn std::error::Error>` in libraries unless it is really needed181