Skip to content

Closures & Iterators

TypeScript arrow functions vs Rust closures

Section titled “TypeScript arrow functions vs Rust closures”

In TypeScript, arrow functions can capture variables from their enclosing scope freely. Rust closures do the same, but the compiler tracks exactly how the closure uses its captured values and enforces ownership rules at compile time.

TypeScript
// TypeScript — arrow function captures 'factor' by reference
const factor = 3;
const triple = (x: number) => x * factor;
console.log(triple(5)); // 15
// Closures in higher-order functions
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// Capturing mutable state — counter
let count = 0;
const increment = () => { count++; };
increment();
increment();
console.log(count); // 2
Rust
fn main() {
// Closure captures 'factor' by reference (immutable)
let factor = 3;
let triple = |x| x * factor;
println!("{}", triple(5)); // 15
// Iterator adapters: map, filter, collect
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// FnMut closure — captures count mutably
let mut count = 0;
let mut increment = || { count += 1; };
increment();
increment();
drop(increment); // release borrow so we can use count again
println!("{count}"); // 2
}

Rust models closure capabilities as three traits. Understanding them is important when you pass closures to functions or store them in structs.

TraitWhat it can doTypeScript analogy
FnOnceCalled at most once; can move captured values outA one-shot callback that consumes its captures
FnMutCalled multiple times; mutates capturesA stateful callback that modifies closed-over variables
FnCalled multiple times; only reads captures (or captures nothing)A pure arrow function with no side effects on captured vars

Every Fn is also FnMut; every FnMut is also FnOnce. When you take a &dyn Fn(T) -> U parameter, you accept the most restrictive requirement — the closure must be callable multiple times without mutating.

fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
fn main() {
let double = |x| x * 2;
println!("{}", apply_twice(double, 3)); // 12
}

In TypeScript, async () => { ... } is just a function that returns a promise, and nothing about the syntax changes. In Rust the equivalent was awkward for years: a closure returning async { ... } produces a future that cannot borrow from the closure’s captures, so anything the future touched had to be cloned or moved.

Rust 1.85 added real async closures — async || { ... } — along with the AsyncFn, AsyncFnMut and AsyncFnOnce traits that mirror the three Fn traits above. The future they return is allowed to borrow from the captures, which is what makes the natural code compile.

TypeScript
// TypeScript: async arrow function
const runTwice = async (f: (url: string) => Promise<number>) =>
(await f("https://a.example")) + (await f("https://bb.example"));
const log: string[] = [];
const total = await runTwice(async (url) => {
const n = await fetchLen(url);
log.push(`${url} -> ${n}`); // closes over 'log' freely
return n;
});
Rust
// Rust: async closure, bound with AsyncFnMut
async fn run_twice<F>(mut f: F) -> usize
where
F: AsyncFnMut(&str) -> usize,
{
f("https://a.example").await + f("https://bb.example").await
}
let mut log: Vec<String> = Vec::new();
let total = run_twice(async |url: &str| {
let n = fetch_len(url).await;
log.push(format!("{url} -> {n}")); // borrows 'log' across the await
n
})
.await;

Take the bound that matches how the closure is used, exactly as with Fn/FnMut/FnOnce: AsyncFn if it only reads its captures, AsyncFnMut if it mutates them (the example pushes to log), AsyncFnOnce if it consumes them.

// @skip-verify crate: tokio
async fn fetch_len(url: &str) -> usize {
url.len() // pretend this awaits the network
}
// Takes an async closure: AsyncFnMut can mutate what it captures.
async fn run_twice<F>(mut f: F) -> usize
where
F: AsyncFnMut(&str) -> usize,
{
f("https://a.example").await + f("https://bb.example").await
}
#[tokio::main]
async fn main() {
let mut log: Vec<String> = Vec::new();
let total = run_twice(async |url: &str| {
let n = fetch_len(url).await;
log.push(format!("{url} -> {n}"));
n
})
.await;
println!("total = {total}");
for line in &log {
println!("{line}");
}
}

TypeScript’s .map(), .filter(), and .reduce() are eager: they allocate a new array at each step. Rust’s iterator adapters are lazy: nothing is computed until you call a consuming method like .collect(), .sum(), or .for_each(). This means a long chain of .map().filter().take() allocates no intermediate vectors.

TypeScript
// TypeScript — each step allocates a new array
const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(x => x % 2 === 0) // new array [2,4,6,8,10]
.map(x => x * x) // new array [4,16,36,64,100]
.slice(0, 3); // new array [4,16,36]
console.log(result);
Rust
fn main() {
// Rust — no intermediate allocations until collect()
let result: Vec<i32> = (1..=10)
.filter(|x| x % 2 == 0) // lazy
.map(|x| x * x) // lazy
.take(3) // lazy
.collect(); // ONE allocation here
println!("{:?}", result); // [4, 16, 36]
// Common adapters
let sum: i32 = (1..=100).sum();
println!("sum 1..100 = {sum}");
let words = vec!["hello", "world", "rust"];
let upper: Vec<String> = words.iter()
.map(|w| w.to_uppercase())
.collect();
println!("{:?}", upper);
// flat_map (like Array.flatMap)
let sentences = vec!["hello world", "foo bar"];
let all_words: Vec<&str> = sentences.iter()
.flat_map(|s| s.split_whitespace())
.collect();
println!("{:?}", all_words);
}
fn main() {
let multiplier = 3;
let triple = |x| x * multiplier;
println!("triple(5) = {}", triple(5));
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens_doubled: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect();
println!("evens_doubled = {:?}", evens_doubled);
let sum: i32 = numbers.iter().sum();
println!("sum = {sum}");
let result: Vec<String> = (1..=3)
.map(|x| x * x)
.map(|x| format!("sq:{x}"))
.collect();
println!("{:?}", result);
}
Which `Fn` trait should you use when a closure only reads (does not mutate) its captured variables and can be called multiple times?
When does Rust's iterator chain actually execute its computations?
What does the `move` keyword do when used before a closure?
Which Rust iterator adapter is equivalent to JavaScript's `Array.prototype.flatMap`?