Skip to content
Learn Rust
Theme

Idioms and design patterns

Going further · Lesson 53

An idiom is the way Rust code usually says a common thing: wrap a number in a type of its own, build a value in one place that checks it, make a wrong state impossible to write. Many are design patterns you already know, trimmed because enums, traits and ownership do part of the work. This page shows the ones you will read most, the compiler error each one turns a mistake into, and the habits that look like idioms and are not.

Coming from GoA defined type (type UserID int) is the newtype, and functional options are the cousin of the Rust builder. Go chose the zero value on purpose (var p Port is 0), and most Go types are designed so it is valid; the cost shows when it should not be, so "a Port that cannot be invalid" is a convention there. A Rust type has no zero value unless you write one, so a Port comes from the function you wrote. Cleanup is also different: defer runs at the end of the function, a Rust Drop guard at the end of its scope.

Coming from Kotlinvalue class is the newtype, default and named arguments plus copy do much of what a Rust builder or ..Default::default() does, and a sealed hierarchy is an enum. Rust has no named or default arguments, so the builder is more common than in Kotlin. use {} is the nearest to a Drop guard, but it is written by the caller at each use; a Rust guard runs without it.

Coming from PythonThe same designs exist, checked by the compiler where Python relies on convention. typing.NewType is erased at run time (the page below passes an order id to a function that wants a user id, and it runs). A class that checks in __init__ or __post_init__ is the nearest Python form of "parse, don't validate": no invalid Port gets built, but the failure is an exception, so nothing makes the caller handle it. A with block is the nearest to a Drop guard, but the caller has to write it.

Coming from TypeScriptBranded types and discriminated unions are your newtype and enum, and tsc --strict checks them. They are erased when the program runs (the page below prints typeof of a branded id: number). A Rust newtype takes no more space than its field, so it is gone at run time too; what differs is the build, where no cast turns a number into a Port.

Coming from JavaA record around one field is the newtype, a nested Builder class is the builder, and sealed interfaces with a switch are enums. A Strategy is already an interface or a lambda in Java, and a Rust trait or closure is the same thing; State has no base class to subclass per state, so it is an enum or a typestate. try-with-resources is the nearest to Drop, but you can forget to write the try.

Coming from C#A readonly record struct is the newtype, init properties with with expressions do the job of a builder and ..Default::default(), and using is the nearest to a Drop guard. A Rust type has no constructors: an associated function such as new or parse is an ordinary function that returns the value, so it can return a Result.

Coming from Ctypedef int UserId; names a type without making one: the page below passes an order id to a function that wants a user id, and it compiles under -Wall -Wextra. A one-field Rust struct is a new type. Designated initializers ({.port = 9000}) give C zero for everything you leave out, and cleanup is goto cleanup by hand, where Rust runs Drop for you.

Coming from C++RAII is where Drop comes from, an explicit one-field struct is the newtype, and std::variant is an enum. What changes is that a Rust function builds the value, so it can return a Result the caller has to handle (a C++ constructor has no return value, so it throws), and a moved-from value cannot be used.

The quick comparison

Rustsrc/main.rsRun ↗
mod port {
/// A port from 1 to 65535. The field is private: only `parse` can build one.
pub struct Port(u16);
impl Port {
pub fn parse(text: &str) -> Result<Port, String> {
let n: u32 = text.parse().map_err(|_| "not a number".to_string())?;
if n == 0 || n > 65535 {
return Err(format!("{n} is outside 1-65535"));
}
Ok(Port(n as u16))
}
pub fn get(&self) -> u16 {
self.0
}
}
}
use port::Port;
// A Port is already valid, so this function has nothing to check.
fn listen(port: &Port) {
println!("listening on {}", port.get());
}
fn main() {
for text in ["8080", "0", "http"] {
match Port::parse(text) {
Ok(port) => listen(&port),
Err(e) => println!("{text:?}: {e}"),
}
}
}
$ cargo run
listening on 8080
"0": 0 is outside 1-65535
"http": not a number
package main
import (
"fmt"
"strconv"
)
// Port is valid by construction, if every caller goes through ParsePort.
// In package main the field is visible to the whole package; in its own
// package, `n` would be hidden from other packages.
type Port struct{ n uint16 }
func ParsePort(text string) (Port, error) {
n, err := strconv.ParseUint(text, 10, 32)
if err != nil {
return Port{}, fmt.Errorf("not a number")
}
if n == 0 || n > 65535 {
return Port{}, fmt.Errorf("%d is outside 1-65535", n)
}
return Port{uint16(n)}, nil
}
func listen(p Port) {
fmt.Println("listening on", p.n)
}
func main() {
for _, text := range []string{"8080", "0", "http"} {
port, err := ParsePort(text)
if err != nil {
fmt.Printf("%q: %v\n", text, err)
continue
}
listen(port)
}
}
$ go run main.go
listening on 8080
"0": 0 is outside 1-65535
"http": not a number
// The constructor is private, so Port.parse is the only way in.
class Port private constructor(val number: Int) {
companion object {
fun parse(text: String): Port {
val n = text.toLongOrNull() ?: throw IllegalArgumentException("not a number")
if (n < 1 || n > 65535) throw IllegalArgumentException("$n is outside 1-65535")
return Port(n.toInt())
}
}
}
fun listen(port: Port) {
println("listening on ${port.number}")
}
fun main() {
for (text in listOf("8080", "0", "http")) {
try {
listen(Port.parse(text))
} catch (e: IllegalArgumentException) {
println("\"$text\": ${e.message}")
}
}
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
listening on 8080
"0": 0 is outside 1-65535
"http": not a number
class Port:
"""The leading underscore is a convention: Python does not stop Port(0)."""
def __init__(self, number):
self._number = number
@classmethod
def parse(cls, text):
try:
n = int(text)
except ValueError:
raise ValueError("not a number") from None
if n < 1 or n > 65535:
raise ValueError(f"{n} is outside 1-65535")
return cls(n)
@property
def number(self):
return self._number
def listen(port):
print("listening on", port.number)
for text in ["8080", "0", "http"]:
try:
listen(Port.parse(text))
except ValueError as e:
print(f'"{text}": {e}')
$ python3 main.py
listening on 8080
"0": 0 is outside 1-65535
"http": not a number
class Port {
// A private constructor: outside the class, `new Port(0)` does not compile.
private constructor(readonly number: number) {}
static parse(text: string): Port {
if (!/^\d+$/.test(text)) throw new Error("not a number");
const n = Number(text);
if (n < 1 || n > 65535) throw new Error(`${n} is outside 1-65535`);
return new Port(n);
}
}
function listen(port: Port): void {
console.log(`listening on ${port.number}`);
}
for (const text of ["8080", "0", "http"]) {
try {
listen(Port.parse(text));
} catch (e) {
console.log(`"${text}": ${(e as Error).message}`);
}
}
$ tsc --strict --module nodenext --types node main.ts && node main.js
listening on 8080
"0": 0 is outside 1-65535
"http": not a number
public class Main {
// The constructor is private, so Port.parse is the only way in.
static final class Port {
private final int number;
private Port(int number) {
this.number = number;
}
static Port parse(String text) {
long n;
try {
n = Long.parseLong(text);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("not a number");
}
if (n < 1 || n > 65535) {
throw new IllegalArgumentException(n + " is outside 1-65535");
}
return new Port((int) n);
}
int number() {
return number;
}
}
static void listen(Port port) {
System.out.println("listening on " + port.number());
}
public static void main(String[] args) {
for (String text : new String[] {"8080", "0", "http"}) {
try {
listen(Port.parse(text));
} catch (IllegalArgumentException e) {
System.out.println("\"" + text + "\": " + e.getMessage());
}
}
}
}
$ javac Main.java && java Main
listening on 8080
"0": 0 is outside 1-65535
"http": not a number
// The constructor is private, so Port.Parse is the only way in.
class Port
{
public int Number { get; }
private Port(int number) { Number = number; }
public static Port Parse(string text)
{
if (!long.TryParse(text, out var n)) throw new FormatException("not a number");
if (n < 1 || n > 65535) throw new FormatException($"{n} is outside 1-65535");
return new Port((int)n);
}
}
class Program
{
static void Listen(Port port) => Console.WriteLine($"listening on {port.Number}");
static void Main()
{
foreach (var text in new[] { "8080", "0", "http" })
{
try { Listen(Port.Parse(text)); }
catch (FormatException e) { Console.WriteLine($"\"{text}\": {e.Message}"); }
}
}
}
$ dotnet run
listening on 8080
"0": 0 is outside 1-65535
"http": not a number
#include <stdio.h>
#include <stdlib.h>
/* The struct is a plain value: nothing stops `struct Port p = {0};`.
In a library, the header would only declare `struct Port;` so callers
could hold a pointer but not build one. */
struct Port {
unsigned short number;
};
/* Returns 0 on success; on failure returns -1 and sets *error. */
static int parse_port(const char *text, struct Port *out, const char **error) {
char *end;
unsigned long n = strtoul(text, &end, 10);
if (*text == '\0' || *end != '\0') {
*error = "not a number";
return -1;
}
if (n < 1 || n > 65535) {
static char message[64];
snprintf(message, sizeof message, "%lu is outside 1-65535", n);
*error = message;
return -1;
}
out->number = (unsigned short)n;
return 0;
}
static void listen_on(const struct Port *port) {
printf("listening on %d\n", port->number);
}
int main(void) {
const char *inputs[] = {"8080", "0", "http"};
for (int i = 0; i < 3; i++) {
struct Port port;
const char *error;
if (parse_port(inputs[i], &port, &error) == 0) {
listen_on(&port);
} else {
printf("\"%s\": %s\n", inputs[i], error);
}
}
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
listening on 8080
"0": 0 is outside 1-65535
"http": not a number
#include <iostream>
#include <stdexcept>
#include <string>
// The constructor is private, so Port::parse is the only way in.
class Port {
public:
static Port parse(const std::string& text) {
std::size_t used = 0;
unsigned long n = 0;
try {
n = std::stoul(text, &used);
} catch (const std::exception&) {
throw std::invalid_argument("not a number");
}
if (used != text.size()) throw std::invalid_argument("not a number");
if (n < 1 || n > 65535) throw std::invalid_argument(std::to_string(n) + " is outside 1-65535");
return Port(static_cast<unsigned short>(n));
}
unsigned short number() const { return number_; }
private:
explicit Port(unsigned short number) : number_(number) {}
unsigned short number_;
};
void listen_on(const Port& port) {
std::cout << "listening on " << port.number() << "\n";
}
int main() {
for (const std::string text : {"8080", "0", "http"}) {
try {
listen_on(Port::parse(text));
} catch (const std::invalid_argument& e) {
std::cout << '"' << text << "\": " << e.what() << "\n";
}
}
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
listening on 8080
"0": 0 is outside 1-65535
"http": not a number

What's different

  • A newtype is a struct with one field. It is a different type from the field's type, so the compiler refuses to mix them up, and its field can be private so that your own functions build it.
  • "Parse, don't validate": turn loosely typed input into a type that cannot be invalid, once, at the edge; everything after takes the strong type.
  • Enums with data make illegal states unrepresentable: a download that is both done and failed has no spelling. A typestate goes further and puts the state in the type, so calling send before connect is a compile error.
  • Default with ..Default::default() and a builder stand in for the named and default arguments that Rust does not have. A guard type with a Drop impl ties cleanup to a scope.
  • Some habits look like idioms and are not: a .clone() that silences the borrow checker, unwrap() in a library, Rc<RefCell<_>> for everything, text where an enum belongs, generics nobody uses, and Box<dyn Error> as a library's error type. Each has a before and an after below.
Gotcha

Copying a struct with .. and then using the original

Looks fine: in Go, Kotlin, Python, TypeScript, Java, C#, C and C++ it runs fine. 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.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.
Rustsrc/main.rs✗ won't compile
#[derive(Debug, Clone)]
struct Config {
name: String,
port: u16,
verbose: bool,
}
fn main() {
let base = Config {
name: "service".to_string(),
port: 8080,
verbose: false,
};
// "The same, but on another port."
let test = Config { port: 9000, ..base };
println!("{base:?}");
println!("{test:?}");
}
package main
import "fmt"
type Config struct {
Name string
Port uint16
Verbose bool
}
func main() {
base := Config{Name: "service", Port: 8080, Verbose: false}
test := base // a struct assignment copies the fields
test.Port = 9000
fmt.Printf("%+v\n", base)
fmt.Printf("%+v\n", test)
}
$ go run main.go✓ runs
{Name:service Port:8080 Verbose:false}
{Name:service Port:9000 Verbose:false}
data class Config(val name: String, val port: Int, val verbose: Boolean)
fun main() {
val base = Config("service", 8080, false)
val test = base.copy(port = 9000)
println(base)
println(test)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
Config(name=service, port=8080, verbose=false)
Config(name=service, port=9000, verbose=false)
from dataclasses import dataclass, replace
@dataclass
class Config:
name: str
port: int
verbose: bool
base = Config("service", 8080, False)
test = replace(base, port=9000)
print(base)
print(test)
$ python3 main.py✓ runs
Config(name='service', port=8080, verbose=False)
Config(name='service', port=9000, verbose=False)
interface Config {
name: string;
port: number;
verbose: boolean;
}
const base: Config = { name: "service", port: 8080, verbose: false };
const test: Config = { ...base, port: 9000 };
console.log(base);
console.log(test);
$ tsc --strict --module nodenext --types node main.ts && node main.js✓ runs
{ name: 'service', port: 8080, verbose: false }
{ name: 'service', port: 9000, verbose: false }
public class Main {
record Config(String name, int port, boolean verbose) {}
public static void main(String[] args) {
Config base = new Config("service", 8080, false);
// A record has no "copy with"; you pass the fields you keep.
Config test = new Config(base.name(), 9000, base.verbose());
System.out.println(base);
System.out.println(test);
}
}
$ javac Main.java && java Main✓ runs
Config[name=service, port=8080, verbose=false]
Config[name=service, port=9000, verbose=false]
var baseConfig = new Config("service", 8080, false);
var test = baseConfig with { Port = 9000 };
Console.WriteLine(baseConfig);
Console.WriteLine(test);
record Config(string Name, int Port, bool Verbose);
$ dotnet run✓ runs
Config { Name = service, Port = 8080, Verbose = False }
Config { Name = service, Port = 9000, Verbose = False }
#include <stdio.h>
struct Config {
const char *name;
unsigned short port;
int verbose;
};
int main(void) {
struct Config base = {"service", 8080, 0};
struct Config test = base; /* copies the fields, including the name pointer */
test.port = 9000;
printf("%s %d %d\n", base.name, base.port, base.verbose);
printf("%s %d %d\n", test.name, test.port, test.verbose);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
service 8080 0
service 9000 0
#include <iostream>
#include <string>
struct Config {
std::string name;
unsigned short port;
bool verbose;
};
int main() {
Config base{"service", 8080, false};
Config test = base; // copies every field, the string too
test.port = 9000;
std::cout << base.name << " " << base.port << " " << base.verbose << "\n";
std::cout << test.name << " " << test.port << " " << test.verbose << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
service 8080 0
service 9000 0
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0382?
error[E0382]: borrow of partially moved value: `base`
  --> src/main.rs:16:16
   |
15 |     let test = Config { port: 9000, ..base };
   |                ----------------------------- value partially moved here
16 |     println!("{base:?}");
   |                ^^^^ value borrowed here after partial move
   |
   = note: partial move occurs because `base.name` has type `String`, which does not implement the `Copy` trait
What happens in Go

Go runs this and prints both structs. test := base copies the three fields. The string is shared, which is fine because a Go string cannot change.

What happens in Kotlin

Kotlin runs this and prints both. copy(port = 9000) makes a new Config; the String is immutable, so the two share it.

What happens in Python

Python runs this and prints both. dataclasses.replace builds a new Config; the str is immutable, so the two share it.

What happens in TypeScript

TypeScript runs this and prints both. The spread copies the properties one level deep; the string is a primitive. An object-valued property would be shared by both.

What happens in Java

Java runs this and prints both. A record has no copy-with, so you pass the fields you keep by hand; the String is immutable and shared.

What happens in C#

C# runs this and prints both. with copies the record and changes Port; the string is immutable and shared.

What happens in C

C runs this and prints both. The struct assignment copies the fields, and here name is a pointer to a string literal that both copies share. With a malloced name, freeing both copies would free it twice (not run).

What happens in C++

C++ runs this and prints both. The copy duplicates the std::string too, so both are whole; the allocation is the cost Rust asks you to write as .clone().

What Rust sees

..base takes the fields you did not name: name and verbose. verbose is Copy, so it is copied, but name is a String, so it is moved into test. base is now partly moved, and printing all of it is an error.

Why Rust says no

A String has one owner, and a copy that allocates is something Rust makes you ask for with .clone(). So ..base moves what it cannot copy, and base stays whole when the fields taken were all Copy (name the String yourself and it works; Structs shows the same rule). The fix is ..base.clone() when you want a full copy, or ..Default::default() when you only wanted defaults.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
#[derive(Debug, Clone)]
struct Config {
name: String,
port: u16,
verbose: bool,
}
fn main() {
let base = Config {
name: "service".to_string(),
port: 8080,
verbose: false,
};
// `..base.clone()` hands over a copy, so `base` stays whole.
let test = Config {
port: 9000,
..base.clone()
};
println!("{base:?}");
println!("{test:?}");
println!(
"{} on port {}, verbose: {}",
test.name, test.port, test.verbose
);
}
$ cargo run
Config { name: "service", port: 8080, verbose: false }
Config { name: "service", port: 9000, verbose: false }
service on port 9000, verbose: false

The toolkit

Most of these idioms are not language features: each is ordinary structs, enums, traits and Drop, arranged in a shape that Rust code tends to share, usually to move a rule out of your head and into the compiler. The rest (let ... else, matches!, impl Trait) are small features that shorten them. This page names the shapes and says what each costs. Outputs come from Linux on 64-bit ARM with Rust 1.99.1, a debug build.

If you know the design-pattern names, start here. “Close” is the word: each Rust form does the job, not the whole class diagram.

Pattern In Rust Where
Builder a builder type, or Default with .. Building a value
Strategy a closure or trait: impl Trait when the caller picks, Box<dyn Fn> or Box<dyn Trait> when chosen at run time Traits at the edges
State an enum with match, or typestate Illegal states, Typestate
Adapter, wrapper a newtype that implements the trait you need Newtype
Static factory method an associated function: new, parse, try_from Parse, don’t validate
Resource guard (try-with-resources, using, with) a Drop type Guards
Singleton, global instance a static OnceLock or LazyLock (Shared state), or a value you pass in

Observer, Visitor and Decorator have no row: they are written with traits, closures and enums, as in the Strategy row.

Newtype: a type for each meaning

A newtype is a struct with one field (Structs shows the compiler refusing a mix-up of two). Wrapping adds no data, and it lets you implement a trait on a type you do not own:

Rustsrc/main.rsRun ↗
use std::fmt;
use std::mem::size_of;
struct UserId(u32);
struct OrderId(u32);
fn load_user(id: &UserId) -> String {
format!("user #{}", id.0)
}
/// A wrapper around a foreign type, so that this crate may implement a foreign trait on it.
struct Names(Vec<String>);
impl fmt::Display for Names {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.join(", "))
}
}
fn main() {
let user = UserId(7);
let order = OrderId(7);
println!("{}", load_user(&user));
println!("order #{}", order.0);
println!("size_of::<u32>(): {}", size_of::<u32>());
println!("size_of::<UserId>(): {}", size_of::<UserId>());
println!("size_of::<Names>(): {}", size_of::<Names>());
println!("size_of::<Vec<String>>(): {}", size_of::<Vec<String>>());
println!("{}", Names(vec!["Ada".to_string(), "Grace".to_string()]));
}
$ cargo run
user #7
order #7
size_of::<u32>():     4
size_of::<UserId>():  4
size_of::<Names>():   24
size_of::<Vec<String>>(): 24
Ada, Grace

The sizes match their fields. Names is the second use: the orphan rule lets you implement a trait only if you own the trait or the type, so impl Display for Vec<String> is refused (the Book uses a wrapper). The wrapper starts with none of the inner type’s methods or operators. A private field turns a newtype from a label into a guarantee, as the next section shows.

Parse, don’t validate

A function that returns bool leaves every later function to trust that someone called it. A function that returns a Port hands out proof: the check happens once, where text becomes a value, and listen takes a Port and checks nothing. The phrase is from an essay by Alexis King:

Rustsrc/main.rsRun ↗
use std::str::FromStr;
mod port {
use std::fmt;
use std::str::FromStr;
/// A TCP port from 1 to 65535. The only way to get one is `Port::try_from` or `parse`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Port(u16);
#[derive(Debug, PartialEq)]
pub enum PortError {
NotANumber,
OutOfRange(u32),
}
impl fmt::Display for PortError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PortError::NotANumber => write!(f, "not a number"),
PortError::OutOfRange(n) => write!(f, "{n} is outside 1-65535"),
}
}
}
impl TryFrom<u32> for Port {
type Error = PortError;
fn try_from(n: u32) -> Result<Port, PortError> {
match u16::try_from(n) {
Ok(p) if p != 0 => Ok(Port(p)),
_ => Err(PortError::OutOfRange(n)),
}
}
}
impl FromStr for Port {
type Err = PortError;
fn from_str(s: &str) -> Result<Port, PortError> {
let n: u32 = s.trim().parse().map_err(|_| PortError::NotANumber)?;
Port::try_from(n)
}
}
impl Port {
pub fn get(self) -> u16 {
self.0
}
}
}
use port::Port;
/// No check here: a `Port` is already known to be valid.
fn listen(port: Port) -> String {
format!("listening on {}", port.get())
}
fn main() {
for text in ["8080", "0", "70000", "http"] {
match Port::from_str(text) {
Ok(port) => println!("{text:>6} -> {}", listen(port)),
Err(e) => println!("{text:>6} -> rejected: {e}"),
}
}
}
$ cargo run
  8080 -> listening on 8080
     0 -> rejected: 0 is outside 1-65535
 70000 -> rejected: 70000 is outside 1-65535
  http -> rejected: not a number

Two things make it work. The field is private to the module, so code outside it cannot write Port(0):

Rustsrc/main.rs✗ won't compile
mod port {
pub struct Port(u16);
impl Port {
pub fn get(&self) -> u16 {
self.0
}
}
}
fn main() {
let p = port::Port(0);
println!("{}", p.get());
}
$ cargo buildWhat is E0603?
error[E0603]: tuple struct constructor `Port` is private
  --> src/main.rs:12:19
   |
 2 |     pub struct Port(u16);
   |                     --- a constructor is private if any of the fields is private
...
12 |     let p = port::Port(0);
   |                   ^^^^ private tuple struct constructor
   |
note: the tuple struct constructor `Port` is defined here
  --> src/main.rs:2:5
   |
 2 |     pub struct Port(u16);
   |     ^^^^^^^^^^^^^^^^^^^^^
help: consider making the field publicly accessible
   |
 2 |     pub struct Port(pub u16);
   |                     +++

And the constructor returns a Result, so a caller has to deal with the bad input. impl TryFrom<u32> for Port is the standard name for “convert, and it may fail” (the first exercise writes one), FromStr is what str::parse calls, and From is for conversions that cannot fail (Common traits). Code inside the port module can still build a bad Port, because privacy is per module (Modules): the module is the boundary you have to trust.

Make illegal states unrepresentable

Three fields that must agree, and nothing making them agree:

Rustsrc/main.rsRun ↗
/// Three fields that must agree, and nothing makes them.
#[derive(Debug)]
struct Download {
done: bool,
error: Option<String>,
bytes: Option<u64>,
}
fn describe(d: &Download) -> String {
if d.done {
format!("finished, {} bytes", d.bytes.unwrap_or(0))
} else if let Some(e) = &d.error {
format!("failed: {e}")
} else {
"running".to_string()
}
}
fn main() {
let fine = Download {
done: true,
error: None,
bytes: Some(512),
};
// Compiles. Is it finished or failed? `describe` quietly picks one.
let contradiction = Download {
done: true,
error: Some("timeout".to_string()),
bytes: None,
};
println!("{}", describe(&fine));
println!("{}", describe(&contradiction));
}
$ cargo run
finished, 512 bytes
finished, 0 bytes

contradiction is done and failed at once, and describe quietly picks “finished”. An enum has one variant per state, and each carries only the data that state has (Enums):

Rustsrc/main.rsRun ↗
/// One variant per state; each carries only the data that state has.
#[derive(Debug)]
enum Download {
Running,
Done { bytes: u64 },
Failed { reason: String },
}
fn describe(d: &Download) -> String {
match d {
Download::Running => "running".to_string(),
Download::Done { bytes } => format!("finished, {bytes} bytes"),
Download::Failed { reason } => format!("failed: {reason}"),
}
}
fn main() {
let states = [
Download::Running,
Download::Done { bytes: 512 },
Download::Failed {
reason: "timeout".to_string(),
},
];
for s in &states {
println!("{}", describe(s));
}
// "done and failed at once" has no spelling: there is no variant that holds both.
}
$ cargo run
running
finished, 512 bytes
failed: timeout

There is no spelling for “done and failed”. When you add a state, a match that forgets it and has no _ arm stops compiling (E0004, shown in Match), so the compiler lists the places to update. Java’s sealed types and Kotlin’s when check this too (Enums compares them).

Typestate: the state is in the type

An enum is right when the next state depends on data you learn while running. When the order of calls is known in advance, you can put the state in the type, so a call in the wrong state does not compile. Closed and Open are unit structs, types with no fields. Rust rejects a type parameter that no field uses (E0392, compile-checked), so PhantomData<State> is the field that mentions State and holds no data. connect takes self by value, so the closed connection is gone and an open one comes back; every transition rebuilds the struct:

Rustsrc/main.rsRun ↗
use std::marker::PhantomData;
use std::mem::size_of;
struct Closed;
struct Open;
struct Connection<State> {
host: String,
sent: u32,
state: PhantomData<State>,
}
impl Connection<Closed> {
fn new(host: &str) -> Connection<Closed> {
Connection {
host: host.to_string(),
sent: 0,
state: PhantomData,
}
}
/// Takes `self` by value: the closed connection is gone, an open one comes back.
fn connect(self) -> Connection<Open> {
println!("connecting to {}", self.host);
Connection {
host: self.host,
sent: self.sent,
state: PhantomData,
}
}
}
impl Connection<Open> {
fn send(&mut self, text: &str) {
self.sent += 1;
println!("sent {text:?} (message {})", self.sent);
}
fn close(self) -> Connection<Closed> {
println!("closing after {} messages", self.sent);
Connection {
host: self.host,
sent: self.sent,
state: PhantomData,
}
}
}
fn main() {
let conn = Connection::new("example.org");
let mut conn = conn.connect();
conn.send("hello");
conn.send("world");
let conn = conn.close();
println!(
"size_of::<Connection<Closed>>(): {}",
size_of::<Connection<Closed>>()
);
println!(
"size_of::<Connection<Open>>(): {}",
size_of::<Connection<Open>>()
);
println!(
"size_of::<(String, u32)>(): {}",
size_of::<(String, u32)>()
);
println!("host is still {}", conn.host);
}
$ cargo run
connecting to example.org
sent "hello" (message 1)
sent "world" (message 2)
closing after 2 messages
size_of::<Connection<Closed>>(): 32
size_of::<Connection<Open>>():   32
size_of::<(String, u32)>():      32
host is still example.org

All three sizes match: the state costs no space in this program. Misuse is an error about a method that does not exist yet:

Rustsrc/main.rs✗ won't compile
use std::marker::PhantomData;
struct Closed;
struct Open;
struct Connection<State> {
state: PhantomData<State>,
}
impl Connection<Closed> {
fn new() -> Connection<Closed> {
Connection { state: PhantomData }
}
fn connect(self) -> Connection<Open> {
Connection { state: PhantomData }
}
}
impl Connection<Open> {
fn send(&mut self, text: &str) {
println!("sent {text:?}");
}
}
fn main() {
let mut conn = Connection::new();
conn.send("hello");
let _open = conn.connect();
}
$ cargo buildWhat is E0599?
error[E0599]: no method named `send` found for struct `Connection<Closed>` in the current scope
  --> src/main.rs:28:10
   |
 6 | struct Connection<State> {
   | ------------------------ method `send` not found for this struct
...
28 |     conn.send("hello");
   |          ^^^^ method not found in `Connection<Closed>`
   |
   = note: the method was found for `Connection<Open>`

And because connect consumed the closed connection, using it again is a moved-value error:

Rustsrc/main.rs✗ won't compile
use std::marker::PhantomData;
struct Closed;
struct Open;
struct Connection<State> {
state: PhantomData<State>,
}
impl Connection<Closed> {
fn new() -> Connection<Closed> {
Connection { state: PhantomData }
}
fn connect(self) -> Connection<Open> {
Connection { state: PhantomData }
}
}
fn main() {
let closed = Connection::new();
let _open = closed.connect();
let _again = closed.connect();
}
$ cargo buildWhat is E0382?
error[E0382]: use of moved value: `closed`
  --> src/main.rs:23:18
   |
21 |     let closed = Connection::new();
   |         ------ move occurs because `closed` has type `Connection<Closed>`, which does not implement the `Copy` trait
22 |     let _open = closed.connect();
   |                        --------- `closed` moved due to this method call
23 |     let _again = closed.connect();
   |                  ^^^^^^ value used here after move
   |
note: `Connection::<Closed>::connect` takes ownership of the receiver `self`, which moves `closed`
  --> src/main.rs:15:16
   |
15 |     fn connect(self) -> Connection<Open> {
   |                ^^^^

The price is more code, and a state read from a file or a socket needs an enum around the typed versions. The Rust Book builds the same design as an alternative to the object-oriented State pattern; Cliff Biffle’s essay is a longer tour.

Building a value: Default, update syntax and the builder

Rust has no default or named arguments (What Rust lacks). For a struct with a few public fields, Default and the update syntax .. are enough:

Rustsrc/main.rsRun ↗
#[derive(Debug, Clone, PartialEq)]
struct Config {
host: String,
port: u16,
retries: u32,
verbose: bool,
}
impl Default for Config {
fn default() -> Config {
Config {
host: "localhost".to_string(),
port: 8080,
retries: 3,
verbose: false,
}
}
}
fn main() {
let plain = Config::default();
let custom = Config {
port: 9000,
verbose: true,
..Default::default()
};
println!("{plain:?}");
println!("{custom:?}");
assert_eq!(custom.host, plain.host);
}
$ cargo run
Config { host: "localhost", port: 8080, retries: 3, verbose: false }
Config { host: "localhost", port: 9000, retries: 3, verbose: true }

.. takes the fields you do not name: a Copy field is copied and a String is moved, which is the gotcha above and the rule Structs shows. When only Copy fields are taken, the original stays whole (compile-checked). When fields need checking together, or the list is long, use a builder (Methods shows both shapes of setter). What this page adds is the end of the chain: one build that checks the whole value and returns a Result.

Rustsrc/main.rsRun ↗
use std::time::Duration;
struct Request {
url: String,
method: String,
retries: u32,
timeout: Duration,
}
#[derive(Debug, PartialEq)]
enum BuildError {
MissingUrl,
}
struct RequestBuilder {
url: Option<String>,
method: String,
retries: u32,
timeout: Duration,
}
impl RequestBuilder {
fn new() -> RequestBuilder {
RequestBuilder {
url: None,
method: "GET".to_string(),
retries: 3,
timeout: Duration::from_secs(30),
}
}
// Each setter takes `self` and returns it, so calls chain and nothing is borrowed.
fn url(mut self, url: &str) -> RequestBuilder {
self.url = Some(url.to_string());
self
}
fn method(mut self, method: &str) -> RequestBuilder {
self.method = method.to_string();
self
}
fn retries(mut self, retries: u32) -> RequestBuilder {
self.retries = retries;
self
}
/// The one place that checks the whole value.
fn build(self) -> Result<Request, BuildError> {
let url = self.url.ok_or(BuildError::MissingUrl)?;
Ok(Request {
url,
method: self.method,
retries: self.retries,
timeout: self.timeout,
})
}
}
fn main() {
let ok = RequestBuilder::new()
.url("https://example.org")
.method("POST")
.retries(0)
.build();
match &ok {
Ok(r) => println!(
"{} {} retries={} timeout={:?}",
r.method, r.url, r.retries, r.timeout
),
Err(e) => println!("error: {e:?}"),
}
let missing = RequestBuilder::new().retries(5).build();
println!("{:?}", missing.map(|r| r.url));
}
$ cargo run
POST https://example.org retries=0 timeout=30s
Err(MissingUrl)

The Rust Design Patterns book says the builder is common in Rust “because Rust lacks overloading and default values for function parameters”. For two or three fields, ..Default::default() is shorter.

Guards: cleanup tied to a scope

A guard is a value whose drop undoes something. drop runs when the value leaves its scope, so the cleanup happens on return and on ? as well as at the end (the MutexGuard in Shared state is one, and Smart pointers has the pointers that free on drop). You will rarely write your own: a File or a MutexGuard is usually the guard:

Rustsrc/main.rsRun ↗
use std::cell::Cell;
/// Raises a counter while it is alive. The matching decrement is in `drop`, so it
/// cannot be forgotten and runs on every way out of the scope.
struct Depth<'a> {
level: &'a Cell<u32>,
}
impl<'a> Depth<'a> {
fn enter(level: &'a Cell<u32>, name: &str) -> Depth<'a> {
level.set(level.get() + 1);
println!("{}enter {name}", " ".repeat(level.get() as usize - 1));
Depth { level }
}
}
impl Drop for Depth<'_> {
fn drop(&mut self) {
self.level.set(self.level.get() - 1);
println!("{}leave", " ".repeat(self.level.get() as usize));
}
}
fn parse(level: &Cell<u32>, text: &str) -> Result<u32, std::num::ParseIntError> {
let _guard = Depth::enter(level, "parse");
let n = text.parse::<u32>()?; // an early return: the guard still drops
Ok(n * 2)
}
fn main() {
let level = Cell::new(0);
{
let _outer = Depth::enter(&level, "main");
println!("{:?}", parse(&level, "21"));
println!("{:?}", parse(&level, "x"));
}
println!("level after: {}", level.get());
}
$ cargo run
enter main
  enter parse
  leave
Ok(42)
  enter parse
  leave
Err(ParseIntError { kind: InvalidDigit })
leave
level after: 0

The second parse returned early with ?, and its leave still printed. A guard cannot be forgotten, where try-with-resources, using and with run only if the caller writes them. The exercise below also checks a panic that unwinds (the stack is cleaned up as the panic travels). Common traits compares Drop with defer, try-with-resources, using, with and C++ destructors.

Flat code

Option and Result methods, ?, let ... else, matches! and iterator chains keep code flat; each is taught with its type: Option, Errors, If/else for let ... else, Match and Iterators. A rule of thumb: ? when failure should leave the function, a combinator when a value is only being transformed, match when the branches do different things. The else block of let ... else must not fall through: it ends in return, break, continue or panic!.

Traits at the edges

impl Trait in an argument is shorthand for a generic parameter, so the caller picks the type at compile time. With a closure it is the Strategy pattern without a class per strategy (Closures). To choose the strategy at run time, store it as a Box<dyn Fn(i32) -> bool>, or a Box<dyn Trait> (Traits), as rule does:

Rustsrc/main.rsRun ↗
use std::fmt::Display;
/// The rule is a parameter. Any closure (or function) with this shape fits.
fn count_matching(items: &[i32], keep: impl Fn(i32) -> bool) -> usize {
let mut n = 0;
for &item in items {
if keep(item) {
n += 1;
}
}
n
}
fn is_even(n: i32) -> bool {
n % 2 == 0
}
/// Chosen at run time (say, from a config file): a boxed closure, one type for every rule.
fn rule(name: &str) -> Option<Box<dyn Fn(i32) -> bool>> {
match name {
"even" => Some(Box::new(is_even)),
"big" => Some(Box::new(|n| n > 5)),
_ => None,
}
}
/// `impl Trait` for the item type too: anything that can be iterated and shown.
fn show_all(items: impl IntoIterator<Item = impl Display>) -> String {
let parts: Vec<String> = items.into_iter().map(|i| i.to_string()).collect();
parts.join(" | ")
}
fn main() {
let data = [1, 2, 3, 4, 5, 6, 7, 8];
let limit = 5;
println!("even: {}", count_matching(&data, is_even));
println!("above {limit}: {}", count_matching(&data, |n| n > limit));
println!(
"odd or > 6: {}",
count_matching(&data, |n| n % 2 == 1 || n > 6)
);
for name in ["big", "even", "prime"] {
match rule(name) {
Some(keep) => println!("rule {name}: {}", count_matching(&data, keep)),
None => println!("rule {name}: unknown"),
}
}
println!("{}", show_all(vec![1, 2, 3]));
println!("{}", show_all(["a", "b"]));
println!("{}", show_all(data.iter().take(2)));
}
$ cargo run
even:       4
above 5:    3
odd or > 6: 5
rule big: 3
rule even: 4
rule prime: unknown
1 | 2 | 3
a | b
1 | 2

An extension trait adds methods to a type you do not own, by implementing your own trait for it. The methods exist only where the trait is imported:

Rustsrc/main.rsRun ↗
mod text_ext {
/// Our own trait, so we may implement it for the foreign type `str`.
pub trait StrExt {
fn is_blank(&self) -> bool;
fn first_word(&self) -> &str;
}
impl StrExt for str {
fn is_blank(&self) -> bool {
self.trim().is_empty()
}
fn first_word(&self) -> &str {
self.split_whitespace().next().unwrap_or("")
}
}
}
// The methods exist only where the trait is in scope.
use text_ext::StrExt;
fn main() {
println!("{}", " ".is_blank());
println!("{}", " hello world".first_word());
let owned = String::from("rust language");
println!("{}", owned.first_word()); // a String derefs to str
}
$ cargo run
true
hello
rust
Rustsrc/main.rs✗ won't compile
mod text_ext {
pub trait StrExt {
fn is_blank(&self) -> bool;
}
impl StrExt for str {
fn is_blank(&self) -> bool {
self.trim().is_empty()
}
}
}
fn main() {
println!("{}", " ".is_blank());
}
$ cargo buildWhat is E0599?
error[E0599]: no method named `is_blank` found for reference `&'static str` in the current scope
  --> src/main.rs:14:26
   |
14 |     println!("{}", "   ".is_blank());
   |                          ^^^^^^^^ method not found in `&'static str`
   |
   = help: items from traits can only be used if the trait is in scope
help: trait `StrExt` which provides `is_blank` is implemented but not in scope; perhaps you want to import it
   |
 1 + use crate::text_ext::StrExt;

A sealed trait is public, but outsiders cannot implement it, because it has a supertrait in a private module that they cannot name. The API Guidelines say it lets you add methods without breaking users. Here only the shapes module can implement Shape; a library would put the private module at its root. An impl from outside fails, and the error says why:

Rustsrc/main.rs✗ won't compile
mod shapes {
mod private {
pub trait Sealed {}
}
pub trait Shape: private::Sealed {
fn sides(&self) -> u32;
}
}
struct Circle;
impl shapes::Shape for Circle {
fn sides(&self) -> u32 {
0
}
}
fn main() {
let _circle = Circle;
}
$ cargo buildWhat is E0277?
error[E0277]: the trait bound `Circle: Sealed` is not satisfied
  --> src/main.rs:13:24
   |
13 | impl shapes::Shape for Circle {
   |                        ^^^^^^ unsatisfied trait bound
   |
help: the trait `Sealed` is not implemented for `Circle`
  --> src/main.rs:11:1
   |
11 | struct Circle;
   | ^^^^^^^^^^^^^
help: this trait has no implementations, consider adding one
  --> src/main.rs:3:9
   |
 3 |         pub trait Sealed {}
   |         ^^^^^^^^^^^^^^^^
note: required by a bound in `Shape`
  --> src/main.rs:6:22
   |
 6 |     pub trait Shape: private::Sealed {
   |                      ^^^^^^^^^^^^^^^ required by this bound in `Shape`
   = note: `Shape` is a "sealed trait", because to implement it you also need to implement `shapes::private::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it

Cow<str> holds either a borrowed or an owned string, so a function can return its input untouched when nothing changed and build a new String only when it must (Strings lists it).

Habits that look like idioms

Rust Design Patterns has a chapter of anti-patterns. These six look like help; each pair below is a real before and after.

.clone() to quiet the borrow checker

The error says team.bonus() borrows all of team while team.scores is borrowed for the loop:

Rustsrc/main.rs✗ won't compile
struct Team {
scores: Vec<u32>,
bonus_per_member: u32,
}
impl Team {
fn bonus(&self) -> u32 {
self.bonus_per_member + self.scores.len() as u32
}
}
fn award(team: &mut Team) {
for score in team.scores.iter_mut() {
*score += team.bonus();
}
}
fn main() {
let mut team = Team {
scores: vec![10, 20, 30],
bonus_per_member: 1,
};
award(&mut team);
println!("{:?}", team.scores);
}
$ cargo buildWhat is E0502?
error[E0502]: cannot borrow `*team` as immutable because it is also borrowed as mutable
  --> src/main.rs:14:19
   |
13 |     for score in team.scores.iter_mut() {
   |                  ----------------------
   |                  |
   |                  mutable borrow occurs here
   |                  mutable borrow later used here
14 |         *score += team.bonus();
   |                   ^^^^ immutable borrow occurs here

Cloning the list makes the error go away and the program wrong, because the bonus lands on a copy that is then thrown away:

Rustsrc/main.rsRun ↗
struct Team {
scores: Vec<u32>,
bonus_per_member: u32,
}
impl Team {
fn bonus(&self) -> u32 {
self.bonus_per_member + self.scores.len() as u32
}
}
fn award(team: &mut Team) {
// "Fixes" the borrow error by working on a copy. The copy is thrown away.
let mut scores = team.scores.clone();
for score in scores.iter_mut() {
*score += team.bonus();
}
}
fn main() {
let mut team = Team {
scores: vec![10, 20, 30],
bonus_per_member: 1,
};
award(&mut team);
println!("{:?}", team.scores);
}
$ cargo run
[10, 20, 30]

The book says the same: “Any changes between the two are not synchronized” (Clone to satisfy the borrow checker). The fix is to read the one number first (Working with the borrow checker):

Rustsrc/main.rsRun ↗
struct Team {
scores: Vec<u32>,
bonus_per_member: u32,
}
impl Team {
fn bonus(&self) -> u32 {
self.bonus_per_member + self.scores.len() as u32
}
}
fn award(team: &mut Team) {
// Read the one number first (a `u32` is copied out, so no borrow is left), then change the list.
let bonus = team.bonus();
for score in team.scores.iter_mut() {
*score += bonus;
}
}
fn main() {
let mut team = Team {
scores: vec![10, 20, 30],
bonus_per_member: 1,
};
award(&mut team);
println!("{:?}", team.scores);
}
$ cargo run
[14, 24, 34]

A clone is sometimes right, but it is a decision about two owners, not a way to end an error.

unwrap in a library

A library that unwraps decides for every caller that bad input is fatal, and the message points into the library:

Rustsrc/main.rs! panics at runtimeRun ↗
/// A "library" function: it decides, for every caller, that bad input is fatal.
pub fn parse_port(text: &str) -> u16 {
text.trim().parse().unwrap()
}
fn main() {
println!("{}", parse_port("8080"));
println!("{}", parse_port("http"));
}
$ cargo run
8080

thread 'main' (1) panicked at src/main.rs:3:25:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Return the Result, and the caller decides (Errors says when expect is right):

Rustsrc/main.rsRun ↗
use std::num::ParseIntError;
/// The caller gets the failure as a value and decides what it means.
pub fn parse_port(text: &str) -> Result<u16, ParseIntError> {
text.trim().parse()
}
fn main() {
for text in ["8080", "http"] {
match parse_port(text) {
Ok(port) => println!("{text}: port {port}"),
Err(e) => println!("{text}: {e}, using 80"),
}
}
}
$ cargo run
8080: port 8080
http: invalid digit found in string, using 80

Rc<RefCell<_>> for everything

It compiles, and the borrow rules become run-time panics:

Rustsrc/main.rs! panics at runtimeRun ↗
use std::cell::RefCell;
use std::rc::Rc;
type Log = Rc<RefCell<Vec<String>>>;
fn note(log: &Log, text: String) {
log.borrow_mut().push(text);
}
/// Adds a follow-up note for every note so far.
fn add_follow_ups(log: &Log) {
for line in log.borrow().iter() {
note(log, format!("follow up on {line}"));
}
}
fn main() {
let log: Log = Rc::new(RefCell::new(vec!["call Ada".to_string()]));
add_follow_ups(&log);
println!("{:?}", log.borrow());
}
$ cargo run
thread 'main' (1) panicked at src/main.rs:7:9:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

If you come from a garbage-collected language, this is what shared mutable references everywhere look like in Rust. Pass &mut down and let the compiler check the same rule. A loop that passes a plain &mut Vec to a function while iterating it is E0502 (compile-checked), and building the new notes first fixes it. Rc<RefCell<_>> is for the cases Smart pointers lists:

Rustsrc/main.rsRun ↗
fn add_follow_ups(log: &mut Vec<String>) {
// Build the new notes first, then add them: the list is never read and changed at once.
let follow_ups: Vec<String> = log
.iter()
.map(|line| format!("follow up on {line}"))
.collect();
log.extend(follow_ups);
}
fn main() {
let mut log = vec!["call Ada".to_string()];
add_follow_ups(&mut log);
println!("{log:?}");
}
$ cargo run
["call Ada", "follow up on call Ada"]

Text where an enum belongs

Any string compiles, and a typo falls into the default arm:

Rustsrc/main.rsRun ↗
/// Stringly typed: any text compiles, and a typo falls through to the default arm.
fn open_flags(mode: &str) -> &'static str {
match mode {
"read" => "O_RDONLY",
"write" => "O_WRONLY",
_ => "O_RDONLY",
}
}
fn main() {
println!("read -> {}", open_flags("read"));
println!("write -> {}", open_flags("write"));
println!("wrtie -> {}", open_flags("wrtie"));
}
$ cargo run
read  -> O_RDONLY
write -> O_WRONLY
wrtie -> O_RDONLY

Parse the text into an enum once, at the edge. The typo becomes an error you can show, and inside the program a typo like Mode::Wrtie does not compile (E0599, compile-checked):

Rustsrc/main.rsRun ↗
use std::str::FromStr;
#[derive(Debug, Clone, Copy)]
enum Mode {
Read,
Write,
}
impl FromStr for Mode {
type Err = String;
/// Text is turned into a `Mode` once, at the edge of the program.
fn from_str(s: &str) -> Result<Mode, String> {
match s {
"read" => Ok(Mode::Read),
"write" => Ok(Mode::Write),
other => Err(format!("unknown mode {other:?}")),
}
}
}
fn open_flags(mode: Mode) -> &'static str {
match mode {
Mode::Read => "O_RDONLY",
Mode::Write => "O_WRONLY",
}
}
fn main() {
for text in ["read", "write", "wrtie"] {
match text.parse::<Mode>() {
Ok(mode) => println!("{text} -> {}", open_flags(mode)),
Err(e) => println!("{text} -> {e}"),
}
}
println!("{}", open_flags(Mode::Write));
}
$ cargo run
read -> O_RDONLY
write -> O_WRONLY
wrtie -> unknown mode "wrtie"
O_WRONLY

Generics nobody uses

A parameter nobody varies buys a longer signature, and an error when an argument does not say what it is:

Rustsrc/main.rs✗ won't compile
/// Generic over the element type "in case" a second caller shows up.
fn first_or_zero<T: Copy + Into<u32>>(items: &[T]) -> u32 {
items.first().map_or(0, |&x| x.into())
}
fn main() {
println!("{}", first_or_zero(&[7u32, 8]));
println!("{}", first_or_zero(&[]));
}
$ cargo buildWhat is E0283?
error[E0283]: type annotations needed
 --> src/main.rs:8:20
  |
8 |     println!("{}", first_or_zero(&[]));
  |                    ^^^^^^^^^^^^^ --- type must be known at this point
  |                    |
  |                    cannot infer type of the type parameter `T` declared on the function `first_or_zero`
  |
  = note: the type must implement `Copy`
  = note: the type must also implement `Into<u32>`
note: required by a bound in `first_or_zero`
 --> src/main.rs:2:21
  |
2 | fn first_or_zero<T: Copy + Into<u32>>(items: &[T]) -> u32 {
  |                     ^^^^ required by this bound in `first_or_zero`
note: required by a bound in `first_or_zero`
 --> src/main.rs:2:28
  |
2 | fn first_or_zero<T: Copy + Into<u32>>(items: &[T]) -> u32 {
  |                            ^^^^^^^^^ required by this bound in `first_or_zero`
help: consider specifying a concrete type for the type parameter `T`
  |
8 |     println!("{}", first_or_zero::</* Type */>(&[]));
  |                                 ++++++++++++++

With one caller, a concrete type is clearer, and &[u32] already takes a Vec, an array and an empty slice:

Rustsrc/main.rsRun ↗
/// One caller today, so one concrete signature. `&[u32]` accepts a Vec, an array and `&[]`.
fn first_or_zero(items: &[u32]) -> u32 {
items.first().copied().unwrap_or(0)
}
fn main() {
println!("{}", first_or_zero(&[7, 8]));
println!("{}", first_or_zero(&vec![9]));
println!("{}", first_or_zero(&[]));
}
$ cargo run
7
9
0

The API Guidelines ask for generics where they let callers pass more, such as I: IntoIterator<Item = i64>. The anti-pattern is generality with no second caller (Generics).

Box<dyn Error> in a library’s API

The signature says only “some error”, so a caller that wants to react to one failure must guess its concrete type:

Rustsrc/main.rsRun ↗
use std::error::Error;
use std::num::ParseIntError;
/// A library function whose signature says only "some error".
pub fn read_port(text: &str) -> Result<u16, Box<dyn Error>> {
let (key, value) = text.split_once('=').ok_or("no '=' in the line")?;
if key != "port" {
return Err(format!("unexpected key {key:?}").into());
}
Ok(value.parse::<u16>()?)
}
fn main() {
for text in ["port=80", "port=eighty", "host=db", "port"] {
match read_port(text) {
Ok(p) => println!("{text}: {p}"),
Err(e) => {
// To react to one kind of failure, the caller has to guess the concrete type.
if e.downcast_ref::<ParseIntError>().is_some() {
println!("{text}: bad number ({e})");
} else {
println!("{text}: other ({e})");
}
}
}
}
}
$ cargo run
port=80: 80
port=eighty: bad number (invalid digit found in string)
host=db: other (unexpected key "host")
port: other (no '=' in the line)

An error enum lists every failure in the signature, and the caller matches:

Rustsrc/main.rsRun ↗
use std::fmt;
use std::num::ParseIntError;
/// The signature lists every way this can fail.
#[derive(Debug)]
pub enum PortError {
NoEquals,
WrongKey(String),
BadNumber(ParseIntError),
}
impl fmt::Display for PortError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PortError::NoEquals => write!(f, "no '=' in the line"),
PortError::WrongKey(k) => write!(f, "unexpected key {k:?}"),
PortError::BadNumber(e) => write!(f, "bad number: {e}"),
}
}
}
impl std::error::Error for PortError {}
impl From<ParseIntError> for PortError {
fn from(e: ParseIntError) -> PortError {
PortError::BadNumber(e)
}
}
pub fn read_port(text: &str) -> Result<u16, PortError> {
let (key, value) = text.split_once('=').ok_or(PortError::NoEquals)?;
if key != "port" {
return Err(PortError::WrongKey(key.to_string()));
}
Ok(value.parse::<u16>()?)
}
fn main() {
for text in ["port=80", "port=eighty", "host=db", "port"] {
match read_port(text) {
Ok(p) => println!("{text}: {p}"),
Err(PortError::BadNumber(e)) => println!("{text}: bad number ({e})"),
Err(e) => println!("{text}: other ({e})"),
}
}
}
$ cargo run
port=80: 80
port=eighty: bad number (invalid digit found in string)
host=db: other (unexpected key "host")
port: other (no '=' in the line)

Box<dyn Error> is a fine choice in a main or a small program (Errors); Application errors shows the libraries that write the enum for you.

Translating from what you know

The layer 1 program is the parse-don’t-validate idea with an inherent Port::parse, in all nine languages. The blocks below cover newtypes, defaults and builders. Enums with data are compared in Enums, Option methods in Option, and cleanup in Common traits.

Going deeper

An idiom proves one thing

Each idiom that guards something (newtype, parse, enums, typestate, sealed) moves one rule into the compiler, and that rule is all it checks. A Port can be valid and the wrong port. A typestate guards the order of calls and not whether the server is up. A sealed trait stops outside implementations, not wrong ones inside the crate. Reach for an idiom when the rule is easy to break and costly to find later, and leave the rest to tests (Testing).

What an idiom costs

Idiom What you pay Skip it when
Newtype forwarding impls for what you need, no operators until added the value lives in one function
Parse, don’t validate a constructor and an error type the check is one line, used once
Typestate one type per state; run-time states need an enum two states and one method
Builder a mirror type and setters to keep in sync with the struct two or three fields: use ..Default::default()
Guard a type and a Drop impl; drop cannot return an error a plain close() call is enough
Extension trait the trait must be imported at every use a free function reads as well
Sealed trait nobody downstream can add an implementation you want plugins

Where the guidelines are

The Rust API Guidelines checklist names these for reviews: C-NEWTYPE, C-BUILDER, C-CTOR (constructors are static, inherent methods), C-VALIDATE, C-SEALED and C-STRUCT-PRIVATE. Where to put these types, what to mark pub(crate), and how to split a library from a binary is in Project structure.

Practice

Try it

Replace each todo!() and run until it prints all checks passed. The starters compile and fail when run.

A temperature the type keeps valid

Write TryFrom<f64> for Celsius, as TryFrom<u32> for Port does above: NaN and both infinities are NotFinite, anything below -273.15 is BelowAbsoluteZero, and -273.15 itself is allowed. The field stays private. A check that only tests degrees < -273.15 compiles and fails, because NaN is not less than anything.

Starterexercises/celsius.rs
mod temperature {
/// A temperature in degrees Celsius: a finite number, not below absolute zero (-273.15).
/// The field is private, so `try_from` is the only way to make one.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Celsius(f64);
#[derive(Debug, PartialEq)]
pub enum TempError {
NotFinite,
BelowAbsoluteZero,
}
impl TryFrom<f64> for Celsius {
type Error = TempError;
fn try_from(degrees: f64) -> Result<Celsius, TempError> {
todo!("reject NaN and both infinities, then anything below -273.15")
}
}
impl Celsius {
pub fn degrees(self) -> f64 {
self.0
}
}
}
use temperature::{Celsius, TempError};
fn main() {
assert_eq!(Celsius::try_from(21.5).unwrap().degrees(), 21.5);
assert_eq!(Celsius::try_from(-273.15).unwrap().degrees(), -273.15); // the limit itself is allowed
assert_eq!(
Celsius::try_from(-273.16),
Err(TempError::BelowAbsoluteZero)
);
assert_eq!(Celsius::try_from(-1e9), Err(TempError::BelowAbsoluteZero));
assert_eq!(Celsius::try_from(f64::NAN), Err(TempError::NotFinite));
assert_eq!(Celsius::try_from(f64::INFINITY), Err(TempError::NotFinite));
assert_eq!(
Celsius::try_from(f64::NEG_INFINITY),
Err(TempError::NotFinite)
);
// `try_into` comes for free with `TryFrom`.
let boiling: Result<Celsius, TempError> = 100.0_f64.try_into();
assert_eq!(boiling.map(Celsius::degrees), Ok(100.0));
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
One solutionexercises/celsius.solution.rsRun ↗
mod temperature {
/// A temperature in degrees Celsius: a finite number, not below absolute zero (-273.15).
/// The field is private, so `try_from` is the only way to make one.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Celsius(f64);
#[derive(Debug, PartialEq)]
pub enum TempError {
NotFinite,
BelowAbsoluteZero,
}
impl TryFrom<f64> for Celsius {
type Error = TempError;
fn try_from(degrees: f64) -> Result<Celsius, TempError> {
if !degrees.is_finite() {
return Err(TempError::NotFinite);
}
if degrees < -273.15 {
return Err(TempError::BelowAbsoluteZero);
}
Ok(Celsius(degrees))
}
}
impl Celsius {
pub fn degrees(self) -> f64 {
self.0
}
}
}
use temperature::{Celsius, TempError};
fn main() {
assert_eq!(Celsius::try_from(21.5).unwrap().degrees(), 21.5);
assert_eq!(Celsius::try_from(-273.15).unwrap().degrees(), -273.15); // the limit itself is allowed
assert_eq!(
Celsius::try_from(-273.16),
Err(TempError::BelowAbsoluteZero)
);
assert_eq!(Celsius::try_from(-1e9), Err(TempError::BelowAbsoluteZero));
assert_eq!(Celsius::try_from(f64::NAN), Err(TempError::NotFinite));
assert_eq!(Celsius::try_from(f64::INFINITY), Err(TempError::NotFinite));
assert_eq!(
Celsius::try_from(f64::NEG_INFINITY),
Err(TempError::NotFinite)
);
// `try_into` comes for free with `TryFrom`.
let boiling: Result<Celsius, TempError> = 100.0_f64.try_into();
assert_eq!(boiling.map(Celsius::degrees), Ok(100.0));
println!("all checks passed");
}
$ cargo run
all checks passed

A builder that checks at the end

Finish RequestBuilder: defaults, chained setters where the last call wins, and one build that returns MissingUrl, EmptyUrl (blank after trimming) or UnknownMethod (with the method as given). The known methods are GET, POST, PUT and DELETE in any case, and the built request holds the method in upper case. A setter does not judge: a bad method replaced before build is fine, so checking inside the setters fails.

Starterexercises/request.rs
#[derive(Debug, PartialEq)]
pub struct Request {
pub url: String,
pub method: String,
pub retries: u32,
}
#[derive(Debug, PartialEq)]
pub enum BuildError {
MissingUrl,
EmptyUrl,
UnknownMethod(String),
}
/// Collects settings; `build` checks them all and makes the `Request`.
pub struct RequestBuilder {
// add the fields you need
}
impl RequestBuilder {
/// Defaults: method "GET", 3 retries, no url yet.
pub fn new() -> RequestBuilder {
todo!("start with the defaults")
}
pub fn url(self, url: &str) -> RequestBuilder {
todo!("remember the url; checking it is build's job")
}
/// Any case is accepted; the built request holds it in upper case.
pub fn method(self, method: &str) -> RequestBuilder {
todo!("remember the method as given")
}
pub fn retries(self, retries: u32) -> RequestBuilder {
todo!("remember the count")
}
/// Errors: no url was set (MissingUrl); the url is blank after trimming (EmptyUrl);
/// the method is not GET, POST, PUT or DELETE in any case (UnknownMethod, as given).
pub fn build(self) -> Result<Request, BuildError> {
todo!("check everything here and return the Request")
}
}
fn main() {
let r = RequestBuilder::new().url("http://a").build().unwrap();
assert_eq!(
r,
Request {
url: "http://a".into(),
method: "GET".into(),
retries: 3
}
);
let r = RequestBuilder::new()
.retries(0)
.method("post")
.url("http://b")
.build()
.unwrap();
assert_eq!(
r,
Request {
url: "http://b".into(),
method: "POST".into(),
retries: 0
}
);
// the last call wins
let r = RequestBuilder::new()
.url("http://a")
.url("http://c")
.retries(1)
.retries(2)
.build()
.unwrap();
assert_eq!((r.url.as_str(), r.retries), ("http://c", 2));
assert_eq!(
RequestBuilder::new().retries(5).build(),
Err(BuildError::MissingUrl)
);
assert_eq!(
RequestBuilder::new().url(" ").build(),
Err(BuildError::EmptyUrl)
);
assert_eq!(
RequestBuilder::new()
.url("http://a")
.method("fetch")
.build(),
Err(BuildError::UnknownMethod("fetch".into()))
);
// only GET, POST, PUT and DELETE are known: other words are not
assert_eq!(
RequestBuilder::new()
.url("http://a")
.method("patch")
.build(),
Err(BuildError::UnknownMethod("patch".into()))
);
let r = RequestBuilder::new()
.url("http://a")
.method("put")
.build()
.unwrap();
assert_eq!(r.method, "PUT");
// a setter does not judge: a bad method that is replaced before `build` is fine
let r = RequestBuilder::new()
.method("fetch")
.method("Delete")
.url("http://a")
.build()
.unwrap();
assert_eq!(r.method, "DELETE");
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
One solutionexercises/request.solution.rsRun ↗
#[derive(Debug, PartialEq)]
pub struct Request {
pub url: String,
pub method: String,
pub retries: u32,
}
#[derive(Debug, PartialEq)]
pub enum BuildError {
MissingUrl,
EmptyUrl,
UnknownMethod(String),
}
/// Collects settings; `build` checks them all and makes the `Request`.
pub struct RequestBuilder {
url: Option<String>,
method: String,
retries: u32,
}
impl RequestBuilder {
/// Defaults: method "GET", 3 retries, no url yet.
pub fn new() -> RequestBuilder {
RequestBuilder {
url: None,
method: "GET".to_string(),
retries: 3,
}
}
pub fn url(mut self, url: &str) -> RequestBuilder {
self.url = Some(url.to_string());
self
}
/// Any case is accepted; the built request holds it in upper case.
pub fn method(mut self, method: &str) -> RequestBuilder {
self.method = method.to_string();
self
}
pub fn retries(mut self, retries: u32) -> RequestBuilder {
self.retries = retries;
self
}
/// Errors: no url was set (MissingUrl); the url is blank after trimming (EmptyUrl);
/// the method is not GET, POST, PUT or DELETE in any case (UnknownMethod, as given).
pub fn build(self) -> Result<Request, BuildError> {
let url = self.url.ok_or(BuildError::MissingUrl)?;
if url.trim().is_empty() {
return Err(BuildError::EmptyUrl);
}
let upper = self.method.to_uppercase();
if !["GET", "POST", "PUT", "DELETE"].contains(&upper.as_str()) {
return Err(BuildError::UnknownMethod(self.method));
}
Ok(Request {
url,
method: upper,
retries: self.retries,
})
}
}
fn main() {
let r = RequestBuilder::new().url("http://a").build().unwrap();
assert_eq!(
r,
Request {
url: "http://a".into(),
method: "GET".into(),
retries: 3
}
);
let r = RequestBuilder::new()
.retries(0)
.method("post")
.url("http://b")
.build()
.unwrap();
assert_eq!(
r,
Request {
url: "http://b".into(),
method: "POST".into(),
retries: 0
}
);
// the last call wins
let r = RequestBuilder::new()
.url("http://a")
.url("http://c")
.retries(1)
.retries(2)
.build()
.unwrap();
assert_eq!((r.url.as_str(), r.retries), ("http://c", 2));
assert_eq!(
RequestBuilder::new().retries(5).build(),
Err(BuildError::MissingUrl)
);
assert_eq!(
RequestBuilder::new().url(" ").build(),
Err(BuildError::EmptyUrl)
);
assert_eq!(
RequestBuilder::new()
.url("http://a")
.method("fetch")
.build(),
Err(BuildError::UnknownMethod("fetch".into()))
);
// only GET, POST, PUT and DELETE are known: other words are not
assert_eq!(
RequestBuilder::new()
.url("http://a")
.method("patch")
.build(),
Err(BuildError::UnknownMethod("patch".into()))
);
let r = RequestBuilder::new()
.url("http://a")
.method("put")
.build()
.unwrap();
assert_eq!(r.method, "PUT");
// a setter does not judge: a bad method that is replaced before `build` is fine
let r = RequestBuilder::new()
.method("fetch")
.method("Delete")
.url("http://a")
.build()
.unwrap();
assert_eq!(r.method, "DELETE");
println!("all checks passed");
}
$ cargo run
all checks passed

A guard that closes on each way out

run opens two handles, then finishes, fails with Err or panics. Write the Drop impl so both handles close in reverse order on all three paths, including a panic that unwinds (the stack is cleaned up as the panic travels). Closing by hand before each return fails the panic check.

Starterexercises/guard.rs
use std::cell::RefCell;
use std::panic::{self, AssertUnwindSafe};
type Events = RefCell<Vec<String>>;
/// A made-up resource. Opening it records `open <name>`; closing records `close <name>`.
struct Handle<'a> {
events: &'a Events,
name: &'static str,
}
impl<'a> Handle<'a> {
fn open(events: &'a Events, name: &'static str) -> Handle<'a> {
events.borrow_mut().push(format!("open {name}"));
Handle { events, name }
}
}
impl Drop for Handle<'_> {
fn drop(&mut self) {
todo!("record `close <name>`, so every way out of a scope closes the handle")
}
}
#[derive(Clone, Copy)]
enum Mode {
Fine,
Fail,
Panic,
}
fn run(events: &Events, mode: Mode) -> Result<(), String> {
let _db = Handle::open(events, "db");
let _file = Handle::open(events, "file");
events.borrow_mut().push("work".to_string());
match mode {
Mode::Fine => {}
Mode::Fail => return Err("failed".to_string()),
Mode::Panic => panic!("boom"),
}
events.borrow_mut().push("done".to_string());
Ok(())
}
fn events_of(mode: Mode) -> (Vec<String>, String) {
let events: Events = RefCell::new(Vec::new());
let outcome = panic::catch_unwind(AssertUnwindSafe(|| run(&events, mode)));
let what = match outcome {
Ok(Ok(())) => "ok",
Ok(Err(_)) => "error",
Err(_) => "panic",
};
(events.into_inner(), what.to_string())
}
fn main() {
// Keep the expected "boom" quiet; show any other panic, such as a todo!().
panic::set_hook(Box::new(|info| {
if info.payload().downcast_ref::<&str>() != Some(&"boom") {
eprintln!("{info}");
}
}));
let (events, what) = events_of(Mode::Fine);
assert_eq!(what, "ok");
assert_eq!(
events,
[
"open db",
"open file",
"work",
"done",
"close file",
"close db"
]
);
let (events, what) = events_of(Mode::Fail);
assert_eq!(what, "error");
assert_eq!(
events,
["open db", "open file", "work", "close file", "close db"]
);
let (events, what) = events_of(Mode::Panic);
assert_eq!(what, "panic");
assert_eq!(
events,
["open db", "open file", "work", "close file", "close db"]
);
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
One solutionexercises/guard.solution.rsRun ↗
use std::cell::RefCell;
use std::panic::{self, AssertUnwindSafe};
type Events = RefCell<Vec<String>>;
/// A made-up resource. Opening it records `open <name>`; closing records `close <name>`.
struct Handle<'a> {
events: &'a Events,
name: &'static str,
}
impl<'a> Handle<'a> {
fn open(events: &'a Events, name: &'static str) -> Handle<'a> {
events.borrow_mut().push(format!("open {name}"));
Handle { events, name }
}
}
impl Drop for Handle<'_> {
fn drop(&mut self) {
self.events
.borrow_mut()
.push(format!("close {}", self.name));
}
}
#[derive(Clone, Copy)]
enum Mode {
Fine,
Fail,
Panic,
}
fn run(events: &Events, mode: Mode) -> Result<(), String> {
let _db = Handle::open(events, "db");
let _file = Handle::open(events, "file");
events.borrow_mut().push("work".to_string());
match mode {
Mode::Fine => {}
Mode::Fail => return Err("failed".to_string()),
Mode::Panic => panic!("boom"),
}
events.borrow_mut().push("done".to_string());
Ok(())
}
fn events_of(mode: Mode) -> (Vec<String>, String) {
let events: Events = RefCell::new(Vec::new());
let outcome = panic::catch_unwind(AssertUnwindSafe(|| run(&events, mode)));
let what = match outcome {
Ok(Ok(())) => "ok",
Ok(Err(_)) => "error",
Err(_) => "panic",
};
(events.into_inner(), what.to_string())
}
fn main() {
// Keep the expected "boom" quiet; show any other panic, such as a todo!().
panic::set_hook(Box::new(|info| {
if info.payload().downcast_ref::<&str>() != Some(&"boom") {
eprintln!("{info}");
}
}));
let (events, what) = events_of(Mode::Fine);
assert_eq!(what, "ok");
assert_eq!(
events,
[
"open db",
"open file",
"work",
"done",
"close file",
"close db"
]
);
let (events, what) = events_of(Mode::Fail);
assert_eq!(what, "error");
assert_eq!(
events,
["open db", "open file", "work", "close file", "close db"]
);
let (events, what) = events_of(Mode::Panic);
assert_eq!(what, "panic");
assert_eq!(
events,
["open db", "open file", "work", "close file", "close db"]
);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more