The chain follows the type
You write a script to fetch a URL, parse the HTML, and extract the title. In Python, you chain everything into one fluent line. You try the same pattern in Rust, and the compiler rejects you. Sometimes it complains about moved values. Sometimes it says a method doesn't exist. Sometimes it gives you a type that looks right but refuses to cooperate.
Chaining works in Rust, but the rules are stricter. The chain is only as strong as the type flowing through it. Every method returns a type, and the next method must accept that type. If a method returns a value, you own it. If it returns a reference, you are borrowing. If it returns an Option or Result, you have to handle the wrapper before you can chain further. The compiler enforces these transitions explicitly.
Chaining is composition
Method chaining is function composition. You call a method on the result of the previous call. In Rust, the result type dictates what you can do next.
Think of a factory assembly line. Each station takes a part, does something, and passes it down. If a station outputs a part inside a protective box, the next station needs a box opener. If the station outputs the part directly, the next station grabs it. Rust types are those boxes. Option<T> is a box that might be empty. Result<T, E> is a box that might contain an error. &T is a pointer to a part that lives elsewhere.
The chain works when the output of station N matches the input of station N+1. When types mismatch, the chain breaks. You fix it by converting types, borrowing instead of moving, or using combinators that handle the boxes.
Minimal example: Strings and borrows
Start with a simple chain on strings. This shows how borrowing flows through methods.
fn main() {
// Create an owned String on the heap.
let text = " hello world ".to_string();
// Chain methods. Each step returns the type the next step expects.
// trim() borrows text and returns &str.
// to_uppercase() takes &str and returns a new String.
// len() borrows the new String via Deref and returns usize.
let length = text.trim().to_uppercase().len();
println!("Length: {}", length);
}
The chain succeeds because the types align. trim returns a &str. to_uppercase accepts &str. len works on String because String implements Deref<Target = str>, allowing it to borrow as &str automatically.
Chaining works best when the types flow like water. If the types clash, the chain breaks.
How the compiler sees the chain
The compiler evaluates chains left to right. It checks the return type of each method against the receiver type of the next.
In the example above, text is a String. trim is a method on &str. Rust auto-derefs text to &str to call trim. trim returns &str. to_uppercase is a method on &str. It takes the &str from trim and returns a String. len is a method on &str. Rust auto-derefs the String to &str and calls len.
If you break the flow, the compiler stops you.
fn main() {
let text = "hello".to_string();
// to_uppercase returns a String.
// as_bytes expects &str.
// This works because String derefs to &str.
let bytes = text.to_uppercase().as_bytes();
// If you tried to call a method that requires ownership on the result
// of a borrow, you'd get an error.
// For example, if a method returned &str and the next required String,
// you'd need to clone or convert.
}
Read the return type. If the next method expects a different type, insert a conversion.
Realistic example: Options and Results
Real code deals with failures. Files might not exist. Parsing might fail. Lookups might return nothing. Rust wraps these cases in Option and Result. Chaining through these wrappers requires combinators.
use std::fs;
/// Extracts the first word from a file, returning None if anything fails.
fn get_first_word(path: &str) -> Option<String> {
// read_to_string returns Result<String, Error>.
// ok() converts Result to Option, discarding the error.
// This is a convention when you just want "maybe value" and don't care why it failed.
fs::read_to_string(path)
.ok()
// and_then takes a closure that returns Option.
// It flattens Option<Option<T>> into Option<T>.
// lines() returns an iterator. next() returns Option<&str>.
.and_then(|content| content.lines().next())
// map takes a closure that returns a plain value.
// split_whitespace returns an iterator. next() returns Option<&str>.
// We need to flatten this Option too.
.and_then(|line| line.split_whitespace().next())
// map converts &str to String.
.map(|word| word.to_string())
}
fn main() {
match get_first_word("Cargo.toml") {
Some(word) => println!("First word: {}", word),
None => println!("No word found or file error."),
}
}
The chain uses ok(), and_then, and map. Each combinator handles the wrapper type differently. ok() drops the error. and_then chains when the next step returns an Option. map chains when the next step returns a plain value.
Prefer ? for errors. Chains are for data, not control flow.
The combinator trap: map vs and_then
The most common mistake in Rust chaining is using map when you need and_then.
map applies a function and wraps the result. If the function returns Option<T>, map produces Option<Option<T>>.
and_then applies a function and flattens the result. If the function returns Option<T>, and_then produces Option<T>.
fn main() {
let opt = Some(42);
// map wraps the result.
// The closure returns Option<i32>.
// Result is Option<Option<i32>>.
let nested = opt.map(|n| Some(n * 2));
println!("{:?}", nested); // Some(Some(84))
// and_then flattens.
// Result is Option<i32>.
let flat = opt.and_then(|n| Some(n * 2));
println!("{:?}", flat); // Some(84)
}
If you see Option<Option<T>> or Result<Result<T, E>, E> in your type errors, you used map where you needed and_then. The same rule applies to Result. Use Result::map for plain values. Use Result::and_then for functions that return Result.
Read the return type. If it's Option<Option<T>>, you forgot an and_then.
Borrowing in chains: as_ref and as_mut
Sometimes you need to chain methods that expect references, but you hold an owned value. Or you need to borrow the inner value of an Option without consuming it.
as_ref() converts Option<T> to Option<&T>. as_mut() converts Option<T> to Option<&mut T>. This lets you chain methods that work on references.
fn main() {
let opt = Some(String::from("hello"));
// map moves the String out of the Option.
// opt is now None after this line.
// let len = opt.map(|s| s.len());
// as_ref borrows the inner value.
// opt remains Some(String) after this line.
let len = opt.as_ref().map(|s| s.len());
println!("Length: {}", len);
println!("Opt still valid: {:?}", opt);
}
as_ref() is essential when you need to chain multiple borrows or when the closure needs a reference. The community convention is to use as_ref() whenever you want to inspect or transform the inner value without taking ownership.
Use as_ref() to chain methods that expect references when you currently hold an owned value.
Async chains: await is an operator
Asynchronous methods return Futures. Futures are lazy. They do nothing until you .await them. .await is an operator, not a method. You cannot chain .await directly. You must await the future before calling the next method.
use reqwest;
async fn fetch_title(url: &str) -> Result<String, reqwest::Error> {
// get() returns a Request.
// send() returns a Future<Output = Response>.
// .await resolves the Future to Response.
// ? propagates errors.
let response = reqwest::get(url).await?;
// text() returns a Future<Output = String>.
// .await resolves it to String.
let text = response.text().await?;
// Simple parsing chain on the resolved string.
Ok(text.lines()
.find(|line| line.contains("<title>"))
.unwrap_or("")
.to_string())
}
You cannot write reqwest::get(url).await.text().await in one chain without intermediate bindings or careful structuring, because get returns a Request, not a Future. send returns the Future. You must await send. Then text returns a Future. You must await text.
Async chaining requires explicit .await between every step that returns a future. The compiler will reject missing awaits with E0728 (await is only allowed inside async functions) or type errors if you try to call methods on a Future instead of its output.
Match the tool to the type. The chain follows the data.
Pitfalls and compiler errors
Chaining introduces specific failure modes. Recognize these patterns quickly.
Moved values break chains. If a method consumes the value, you cannot chain on the original variable again.
fn main() {
let vec = vec![1, 2, 3];
let len = vec.len();
// into_iter consumes vec.
let sum: i32 = vec.into_iter().sum();
// E0382: use of moved value `vec`.
// println!("{}", vec.len());
}
The compiler rejects this with E0382 (use of moved value). into_iter takes ownership. vec is gone. Use iter() to borrow instead if you need to keep the value.
Missing borrows cause type mismatches. Methods often expect references. If you pass an owned value, the compiler might try to move it, breaking the chain.
fn main() {
let s = String::from("hello");
// to_uppercase expects &str.
// Passing s by value moves it.
// This works because to_uppercase takes &self, so Rust auto-borrows.
let upper = s.to_uppercase();
// But if a method takes self by value, you must move explicitly.
}
Rust auto-borrows for method calls. s.to_uppercase() is equivalent to (&s).to_uppercase(). This convenience hides the borrow. If you ever need to move, call the function form or use Into.
Nested wrappers indicate wrong combinators. Option<Option<T>> means you used map instead of and_then. Result<Result<T, E>, E> means the same for Result. Fix by switching to and_then.
Async type errors mean missing awaits. If the compiler says a method doesn't exist on a Future, you forgot .await. Futures don't implement the methods of their output types. Await first, then chain.
Read the error message. It tells you the type mismatch. Fix the type, and the chain flows.
Decision matrix
Choose the right chaining pattern based on the types involved.
Use direct chaining with . when each method returns the exact type the next method expects. Use ? to propagate errors out of a function when the chain gets too long or you need to add logic between steps. Use and_then when chaining methods on Option or Result and the next method also returns an Option or Result. Use map when chaining on Option or Result and the next method returns a plain value. Use .await explicitly between every asynchronous method call to resolve the future before the next step. Use as_ref() to chain methods that expect references when you currently hold an owned value. Use iter() instead of into_iter() when you need to keep the collection alive after the chain. Use flatten() when you have a nested Option or Result and want to remove one layer.
Match the tool to the type. The chain follows the data.