Learn / Rust Practical Systems
Error Modeling
Represent expected failures as types and keep surprising failures observable.
Course: Rust Practical Systems. Level: Intermediate. Topic: Systems reliability.
Stage: intermediate - Intermediate error modeling - Typed errors and HTTP response mapping. Represent expected failures as types and route unexpected failures into observable system signals.
Outcomes
- Separate user errors from system errors.
- Map domain errors to HTTP status codes.
- Log context without leaking secrets.
- Choose Option for absence and Result for fallible operations
- Use the question-mark operator for propagation
- Design custom error types and decide where panics are acceptable
Concepts
- Result
- thiserror
- IntoResponse
- tracing
- Error Handling
- Recoverable errors are values
- Error type strategy
- Rust for Reliable Systems and Services foundations
- Guided practice
Concept flow
Show how typed errors and http response mapping moves from trigger to implementation outcome in Rust Systems.
- Domain error
- HTTP mapper
- Problem response
- Trace event
Session flow
- Model Result (concept, 8 min) — Name the decisions behind Result before writing code.
- Separate user errors from system errors.
- Explain where Result belongs in job orchestration service.
- Build the vertical slice (walkthrough, 15 min) — Implement the smallest useful slice in src/error.rs.
- Map domain errors to HTTP status codes.
- Connect thiserror to a working example.
- Verify and harden (exercise, 10 min) — Add structured logging for system failures.
- Log context without leaking secrets.
- Record one risk or follow-up before moving on.
- Result, Option, and Custom Errors: Recoverable errors are values (concept, 53 min) — Rust does not use exceptions for ordinary failure. Option<T> represents present or absent values. Result<T, E> represents success or failure, and callers must handle both paths.
- Retained source example: Propagating errors with question mark
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
let text = fs::read_to_string(path)?;
Ok(text.trim().to_string())
}
The ? operator returns early on Err and unwraps Ok for the next expression.
- Result, Option, and Custom Errors: Error type strategy (walkthrough, 53 min) — Applications often use anyhow for flexible error context at boundaries. Libraries usually expose precise error enums, commonly with thiserror, so callers can match and recover.
- Retained source example: Custom library error
#[derive(Debug)]
enum ConfigError {
MissingKey(String),
InvalidPort(String),
}
fn parse_port(value: &str) -> Result<u16, ConfigError> {
value
.parse::<u16>()
.map_err(|_| ConfigError::InvalidPort(value.to_string()))
}
A typed error preserves the reason and lets callers decide how to handle it.
- Result, Option, and Custom Errors: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Use expect with a helpful invariant message when panicking is truly appropriate.
- Practice: Add context at application boundaries so logs explain what operation failed.
- Practice: Avoid exposing anyhow::Error from reusable libraries unless the abstraction is intentionally opaque.
- Avoid: Calling unwrap in request handlers, background workers, or library code paths.
- Avoid: Using String errors everywhere and losing structured recovery information.
- Avoid: Swallowing errors with ok or unwrap_or defaults that hide operational failures.
- Result, Option, and Custom Errors: references (review, 2 min) — Original references retained from the legacy library.
- Rust book: error handling: https://doc.rust-lang.org/book/ch09-00-error-handling.html
- thiserror: https://docs.rs/thiserror/latest/thiserror/
- anyhow: https://docs.rs/anyhow/latest/anyhow/
Code example
Rust in src/error.rs.
#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("record not found")]
NotFound,
#[error("database unavailable")]
Database(#[from] sqlx::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
match self {
AppError::NotFound => StatusCode::NOT_FOUND.into_response(),
AppError::Database(err) => {
tracing::error!(error = %err, "database failure");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
}
Walkthrough examples
- Error Modeling in a job orchestration service — A team is extending an Axum service with a companion CLI and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: src/error.rs
- File: tests/error-modeling.spec
- File: docs/rust-systems/error-modeling.md
- Start from the provided Rust snippet and make the intent visible in names and boundaries.
- Apply the checklist item "Name expected errors" before adding extra behavior.
- Write down how the implementation changes when thiserror fails or becomes slow.
- Propagating errors with question mark — The ? operator returns early on Err and unwraps Ok for the next expression.
- Retained source code:
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
let text = fs::read_to_string(path)?;
Ok(text.trim().to_string())
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Custom library error — A typed error preserves the reason and lets callers decide how to handle it.
- Retained source code:
#[derive(Debug)]
enum ConfigError {
MissingKey(String),
InvalidPort(String),
}
fn parse_port(value: &str) -> Result<u16, ConfigError> {
value
.parse::<u16>()
.map_err(|_| ConfigError::InvalidPort(value.to_string()))
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Adding context with map_err — ok_or_else converts absence into a Result error without allocating unless needed.
- Retained source code:
fn require_name(input: Option<&str>) -> Result<&str, String> {
input
.filter(|name| !name.trim().is_empty())
.ok_or_else(|| "name is required".to_string())
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Create one domain error enum.
- Map it to HTTP responses.
- Add structured logging for system failures.
- Classify failure shapes: Choose Option or Result for: finding a user by optional id, parsing a port from text, getting the first list item. The guided runner checks the words Option and Result.
- Starter code: find user:
parse port:
first item:
- Hint: Use Result when you need an error reason.
- Hint: Use Option for simple absence.
- Reference solution: find user: Option
parse port: Result
first item: Option
- Accepted answers: Option | Result
Checklist
- Name expected errors
- Map status codes
- Log system context
- Avoid leaking internals
Quiz prompts
- What should a service usually do with unexpected database errors? — Production services need observability for operators and safe responses for clients.
- A teammate wants to hide Result inside a convenient helper. What should you check first? — Place Result at the boundary that keeps job orchestration service behavior explicit, testable, and reviewable.
- Which artifact best proves this Rust Systems lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Intermediate error modeling: a teammate says the happy path works, but "Domain error enum" is still implicit. What should you ask for before merging? — Domain error enum belongs in the intermediate stage only when the decision is visible, testable, and tied to a realistic failure mode.
- A reviewer has five minutes to evaluate this intermediate Rust Systems slice. Which evidence is strongest? — Build an intermediate Rust error layer with typed domain errors, HTTP mapping, tracing for system failures, and response-shape tests.
- When should a library prefer a specific error enum? — Typed errors are part of a library's contract and help callers make precise decisions.
Flashcards
- Intermediate error modeling: what decision does "Domain error enum" force you to make? Name expected application failures before they become stringly typed branches. Evidence prompt: Add one domain error enum with user-safe display text.
- Intermediate error modeling: what decision does "HTTP error mapper" force you to make? Map domain errors to status codes and problem responses without leaking internals. Evidence prompt: Map not-found, conflict, and validation-style errors to distinct responses.
- Intermediate error modeling: what decision does "Tracing system failures" force you to make? Log dependency and unexpected failures with context while keeping response bodies safe. Evidence prompt: Capture one tracing event for a database failure and verify the client response is sanitized.
- In Rust Systems, what should you remember about Result? Result matters here because it supports "Separate user errors from system errors.".
- In Rust Systems, what should you remember about thiserror? thiserror matters here because it supports "Map domain errors to HTTP status codes.".
- In Rust Systems, what should you remember about IntoResponse? IntoResponse matters here because it supports "Log context without leaking secrets.".
- In Rust Systems, what should you remember about tracing? tracing matters here because it supports "Separate user errors from system errors.".
Labs
- Ship a error modeling slice — Extend an Axum service with a companion CLI with a small but reviewable feature that proves the lesson's architecture in code.
- Build an intermediate Rust error layer with typed domain errors, HTTP mapping, tracing for system failures, and response-shape tests.
- Add one domain error enum with user-safe display text.
- Map not-found, conflict, and validation-style errors to distinct responses.
- Capture one tracing event for a database failure and verify the client response is sanitized.
- Create one domain error enum.
- Map it to HTTP responses.
- The lab demonstrates the intermediate error modeling outcome without skipping earlier contract evidence.
- Each subtopic has a concrete artifact: Domain error enum, HTTP error mapper, Tracing system failures.
- The review notes explain how this level changes ownership, verification, or operational risk.
Challenge
- Review-ready error modeling (Stretch) — Build an intermediate Rust error layer with typed domain errors, HTTP mapping, tracing for system failures, and response-shape tests.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from src/error.rs plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL