Problem-solving recipes
A step-by-step procedure for every problem type this course teaches — what to do first, what to do next, and how to tell which case you're in.
What are you being asked to do?
What's in front of you right now?
- The build stopped with a borrow or move error (E0382, E0499, E0502, E0506) → Fix a borrow error. Not sure which line moved the value? Predict what happens to a value at each use first. Only asking where a value is freed? Find the line that frees each value.
- The program panicked → Trace a panic to its cause.
- The error is about lifetimes → E0106 on a function that returns a reference: Write out the lifetimes a signature leaves out, then Annotate a function that returns a reference if elision can't decide. E0597, E0515 or E0716, or "may not live long enough" → Fix a lifetime error, which usually means changing ownership, not annotations.
- E0117, a trait you didn't write on a type you didn't write → Implement a foreign trait for a foreign type (via a newtype).
- You're designing a function or a type: the same calculation appears twice → Turn repeated code into a function. It can fail → Choose how a function reports failure, then Hand an error up with ?. It stores many values → Choose a collection. A value needs the heap or several owners → Choose a smart pointer. Two copies differ only in a type → Make a function generic. Text should turn into your type → Make .parse() work on your type. A sequence built one value at a time → Make your own type an iterator. Copy-and-paste a function can't remove → Write a macro_rules! macro. The whole API → Choose a design for a Rust type or API. Porting from another language → Translate a function into Rust.
- You're organizing a package: logic stuck in
main.rs→ Split a program into a library and a thin main. A new crate → Check a crate before adding it, or the reviewer's version if it's someone else's pull request. One crate needs a fix → Update one dependency on purpose. Old edition → Move a crate to edition 2024. - Your program talks to the world: saving a file that must never be half-written → Replace a file atomically. Settings from flags, env and a file → Layer a command-line tool's settings. The same work on every item → Split a slice's work across threads by hand. A text protocol → Serve a line protocol over TCP with threads. Several SQL statements that belong together → Group statements into a sqlx transaction. The same check or header on every route → Add code that runs around every request. Logs you can't search → Instrument a request handler.
- You're writing tests: a test fails only sometimes → Fix a test that fails only sometimes. A rule should hold for every input → Write a property-based test. The code reads the clock or uses the network → Test code that reads the clock or uses the network. It waits on a timeout or a delay → Test a timeout or a delay without waiting for it. An axum route → Test an axum route without a network.
- Safe Rust can't express it, or C is involved: Wrap an unsafe operation in a safe API, and Review an unsafe block for every one. Calling C → Declare and call a C function from Rust, plus Compile a C file into a Rust package if you ship the C source. C calling you → Hand a Rust object to C as an opaque handle and Export a Rust function C can call safely.
- You're shipping it: Shrink a release binary, Put a Rust service in a small image, Set up CI for a Rust project.
When the compiler says no
Find the line that frees each value
Use when: a program makes heap values such as Strings, and you need to say on which line each buffer goes back
Goal: for each owner, the closing brace that drops its value, and the order of the drops at a shared brace
The ownership rules · extended later in this course
From Chapter 7, before moves. Owners, closing braces, and the order of drops.
Predict what happens to a value at each use
Use when: you're reading a short program and need to say whether it compiles, which names can still be used at a line, or where a value is dropped
Goal: for every use of every value: read, moved, copied, cloned or dropped, and the line of any E0382
Predicting moves
Read a program the way the compiler does, one use at a time. Extends the one above with moves, Copy and clones. The first move before fixing anything.
Fix a borrow error
Use when: rustc stops the build with E0382, E0499, E0502, E0506, E0597, E0106 or E0515
Goal: a program that compiles and still does what it was meant to do
Fixing borrow errors
Read the three labels, then pick the smallest change that keeps the program's meaning.
Write out the lifetimes a signature leaves out
Use when: you want to know what a signature without lifetime names really promises, or why it is E0106
Goal: the signature with every lifetime written in, or the knowledge that you must choose them yourself
Elision rules
Apply the three elision rules by hand to see what a signature really promises.
Annotate a function that returns a reference
Use when: rustc reports E0106 on a function with two or more reference inputs and a reference output
Goal: a signature whose lifetimes say exactly which inputs the result borrows from
Annotating functions
When elision can't choose, name which input the result borrows from.
Fix a lifetime error
Use when: rustc reports E0597, E0515, E0716 or E0499 around a borrow, or says a lifetime may not live long enough, and you're tempted to add or change annotations
Goal: a program whose ownership really keeps every reference valid, with a signature that says only what's true
Common lifetime errors
Most lifetime errors are ownership problems. Annotations come last.
Implement a foreign trait for a foreign type (via a newtype)
Use when: rustc reports E0117 because you want a standard library (or other crate's) trait on a type you didn't define
Goal: the behaviour you wanted, on a local wrapper type the orphan rule allows
Coherence and the orphan rule
The orphan rule's standard way out.
Designing a function or a type
Turn repeated code into a function
Use when: the same calculation appears in two or more places, and only some of the values in it change
Goal: one function with typed parameters and a return type, and a call in place of each copy
Defining and calling functions
Values that change become parameters; the answer is the last line.
Choose how a function reports failure
Use when: you're writing a function that can't always produce its answer, and need to pick its return type
Goal: a panic, a Result or an Option, chosen by who can respond to the failure
Panic or Result?
Decided by who can respond to the failure.
Hand an error up with ?
Use when: a function calls something that returns a Result, and the function itself isn't the right place to decide what to do about a failure
Goal: a function that returns Result and passes every failure to its caller
The question mark operator
Once the function returns Result, ? passes each failure up.
Choose a smart pointer
Use when: a value has to be on the heap, shared by several owners, or changed while it is shared, and you need to pick the type that holds it
Goal: the cheapest pointer that expresses who owns the value and who changes it
Choosing a pointer
Box, Rc, Arc, and the Cell/RefCell/Mutex that go inside them.
Make a function generic
Use when: two or more functions have identical bodies and differ only in the type of value they work on
Goal: one generic function that replaces the copies and compiles
Generic functions
Replace the copies, then add exactly the bounds the body needs.
Make your own type an iterator
Use when: a sequence can be produced one value at a time from a small amount of remembered state, and you'd rather not build all of it in a Vec
Goal: a struct that implements Iterator, so for loops and every adapter and consumer work on it
Writing your own iterator
One next method gives you every adapter for free.
Write a macro_rules! macro
Use when: a job on Section 1's list (any number of arguments, new syntax, generated items, source text) that you'd otherwise copy and paste
Goal: a macro whose expansion is the code you would have written by hand
Declarative macros with macro_rules!
Only after a function or a generic has failed to remove the repetition.
Choose a design for a Rust type or API
Use when: you're about to write a new type, module or API, or you're reviewing one that feels harder to use than it should
Goal: types that make the rules hard to break, with nothing added that the problem doesn't need
Choosing a design
From Chapter 43. Pulls most of the choices above into one pass.
Translate a function into Rust
Use when: you're porting a function from C++, C#, Java, Python, JavaScript or Go and want Rust that compiles and reads as Rust
Goal: a Rust function whose signature says who owns each value, what can be missing and what can fail
The translation table
From Appendix C. For readers arriving from C++, C#, Java, Python, JavaScript or Go.
Organizing a package and its dependencies
Check a crate before adding it
Use when: you're about to add a dependency you haven't used before
Goal: a crate you've looked at on purpose, added with a sensible requirement, with its cost in the graph known
Auditing your dependencies · extended later in this course
Before you add a crate yourself.
Review a pull request that adds a dependency
Use when: a change adds a crate to Cargo.toml, or its Cargo.lock diff shows crates that are new to the graph
Goal: a written decision, in the pull request, on whether each new crate is trusted, based on who publishes it, what it runs and what it brings
Reviewing unsafe and dependencies
Extends the check above for a reviewer, with a written decision per new crate. The most complete version.
Update one dependency on purpose
Use when: a crate you use has a newer compatible release (a bug fix, a security fix) and you want it without moving anything else
Goal: a lock file that differs only in that crate (and anything it drags along), tested and committed
Cargo.lock and builds you can repeat
cargo update -p for one crate, and a lock diff that shows nothing else moved.
Programs that talk to the world
Replace a file atomically
Use when: a program rewrites a file that must never be seen half-written: settings, a summary, a saved game, a database's index
Goal: the file holds either its complete old contents or its complete new contents, whatever happens during the save
Errors and edge cases
Write a temporary file, sync it, then rename over the old one.
Layer a command-line tool's settings
Use when: one setting can come from a built-in default, a config file, an environment variable and a flag
Goal: one merged value per setting, where the most specific source that said something wins
Configuration
Default, then file, then environment, then flag.
Split a slice's work across threads by hand
Use when: the same independent computation runs on every item of a slice, and the partial answers can be combined in any grouping (sum, count, max)
Goal: one combined answer, computed by several scoped threads that each borrowed a chunk
Parallel data processing
Scoped threads, one chunk each. Rayon does the same in one line once you've seen it by hand.
Add code that runs around every request
Use when: the same check or change belongs on many routes, such as a header, a log line or a gate that answers early, and copying it into each handler would miss one
Goal: an async from_fn middleware, added to the router with the right call, at the right place in the layer order
Middleware
One from_fn layer instead of the same lines in every handler.
Instrument a request handler
Use when: a service handles many requests at once and its log lines can't be told apart or searched
Goal: every line a request writes carries that request's facts as fields, at the right level, with nothing private in it
Structured logging and tracing
tracing spans with the request's fields, at the right level, with no secrets.
When a program or a test fails
Trace a panic to its cause
Use when: a program stopped with a message that says panicked at, and you need to find the bug
Goal: the line that panicked, the value that was wrong there, and the line that made that value
Panics
The place line, the reason line, then RUST_BACKTRACE=1 and your first frame.
Fix a test that fails only sometimes
Use when: a test fails now and then in the full suite, or passes only when another test has run first
Goal: tests that each set up their own files, and pass alone, together and in any order
Fixtures and organization
One temporary folder per test, and no test that relies on another.
Write a property-based test
Use when: a function takes inputs from a large range, and you can state a rule its output must follow for every one of them
Goal: a proptest! test that checks the rule on a few hundred generated inputs, and a small failing input if the rule breaks
Parameterized and property-based tests
A rule for every input, checked by proptest, with a small failing case to read.
Test code that reads the clock or uses the network
Use when: a function asks the outside world for something, such as today's date or a message sent, so a direct test would change from day to day or need a real service
Goal: the function taking its outside needs as traits, and fast tests that pass it a stub, a recording mock or a failing double
Mocking with traits
A small trait for each outside need, and a stub, mock or failing double in the test.
Test a timeout or a delay without waiting for it
Use when: async code waits, with a timeout, a sleep or a pause between retries, and a test must prove how long it waits without taking that long
Goal: a #[tokio::test(start_paused = true)] test that runs in no real time and checks each duration to the millisecond
Testing async code
Tokio's paused clock, measured with tokio::time::Instant.
Test an axum route without a network
Use when: you have an axum Router and want a fast test of a route's status, headers or JSON body, with no port and no server
Goal: a #[tokio::test] that builds a request, sends it through the router with oneshot, and checks the status and the body
Testing a service
oneshot on the Router, with the headers a real client sends.
Unsafe code and C
Wrap an unsafe operation in a safe API
Use when: safe Rust refuses something you can argue is correct, such as two &mut into one slice, and no standard-library function already does it
Goal: a safe function whose checks make its unsafe core valid for every input, so callers never write unsafe
Safe abstractions over unsafe code
Checks in safe code make the unsafe core valid for every input.
Review an unsafe block
Use when: a change adds or edits an unsafe block, unsafe fn or unsafe impl
Goal: each unsafe operation is necessary, documented, and valid for every input the safe code around it allows
Checking unsafe code
Run it on every unsafe block, including your own.
Compile a C file into a Rust package
Use when: a Rust package carries C source files that must be compiled and linked into it
Goal: cargo build compiles the C code, links it, and reruns the C step only when a C file changes
Build integration
A build.rs with the cc crate and rerun-if-changed lines.
Hand a Rust object to C as an opaque handle
Use when: C code must create, use and destroy an object whose type is a Rust struct
Goal: C holds only a pointer, every operation goes through Rust, and the object is freed exactly once by Rust's allocator
Ownership across the boundary
Box::into_raw out, Box::from_raw back, exactly once.
Export a Rust function C can call safely
Use when: a Rust function must be callable from C, and it can fail or panic
Goal: C gets a documented status code, the result through an out-parameter, and never a panic
Errors and panics across FFI
Status codes, out-parameters and catch_unwind, so no panic crosses into C.
Shipping it
Shrink a release binary
Use when: a release executable is bigger than you want to download or ship, and you're choosing profile settings
Goal: a [profile.release] where every setting was measured and kept only because it paid for its cost
Release builds and binary size
Measure each profile setting and keep only the ones that pay.