Variables & Types
let — immutable by default
// JS: const is immutable, let is mutable
const score = 42;
const greeting = "Hello, Rust!";
console.log(score);
console.log(greeting); // Rust: let is immutable by default — like JS const
let score = 42;
let greeting = "Hello, Rust!";
println!("{}", score);
println!("{}", greeting); In Rust, variables declared with
let are immutable by default — reassigning them after the first assignment is a compile error. This maps to JavaScript's const, not let. The Rust compiler enforces immutability at compile time, and idiomatic Rust code is primarily immutable: mutation is the exception, not the rule.let mut — mutable variables
let count = 0;
count = count + 1;
count += 1;
console.log(count); let mut count = 0;
count = count + 1;
count += 1;
println!("{}", count); The
mut keyword declares a mutable binding — the exact equivalent of JavaScript's let. Without mut, an attempt to change the value is a compile-time error. The mut keyword makes mutation visible in the code, signaling to readers that this variable's value will change. Idiomatic Rust reaches for mut only when mutation is genuinely required.Type inference and numeric types
const count = 42; // number
const ratio = 3.14; // number (float)
const name = "Alice"; // string
const active = true; // boolean
console.log(typeof count, typeof ratio, typeof name, typeof active); let count = 42i32; // i32: 32-bit signed integer
let ratio = 3.14f64; // f64: 64-bit float
let name = "Alice"; // &str: string slice
let active = true; // bool
println!("{} {} {} {}", count, ratio, name, active); Rust infers types from the literal value just as JavaScript does, but Rust's inference is at compile time and fixes the type permanently. Unlike JavaScript's single
number type, Rust distinguishes integer types (i8, i32, i64, u8, u32, etc.) and floating-point types (f32, f64). The defaults when unspecified are i32 for integers and f64 for floats. JavaScript's typeof inspects the runtime type; in Rust the type is always known at compile time.Explicit type annotations
// JavaScript has no type annotation syntax in plain JS
const productName = "Widget";
const price = 9.99;
const quantity = 100;
const inStock = true;
console.log(productName, price, quantity, inStock); let product_name: &str = "Widget";
let price: f64 = 9.99;
let quantity: u32 = 100;
let in_stock: bool = true;
println!("{} ${:.2} qty:{} in_stock:{}", product_name, price, quantity, in_stock); Rust type annotations follow the variable name with a colon — the same syntax as TypeScript. While Rust's type inference usually makes annotations optional, they are required when the compiler cannot determine the type from context. Rust uses
snake_case for variable names (unlike JavaScript's camelCase), and the compiler emits a warning for names that violate the convention.Constants — required type annotation
const MAX_RETRIES = 3;
const PI_APPROX = 3.14159;
console.log(`Max retries: ${MAX_RETRIES}`);
console.log(`Pi ≈ ${PI_APPROX}`); const MAX_RETRIES: u32 = 3;
const PI_APPROX: f64 = 3.14159;
fn main() {
println!("Max retries: {}", MAX_RETRIES);
println!("Pi ≈ {}", PI_APPROX);
} Rust's
const declares a compile-time constant — the type annotation is required, and the value must be a constant expression computable at compile time. Unlike JavaScript's const (a block-scoped runtime binding), Rust's const is inlined at every use site during compilation. Constants are conventionally named in SCREAMING_SNAKE_CASE in both languages. Rust also has static for module-level variables that have a fixed memory address rather than being inlined.Shadowing — redeclaring with the same name
let measurement = " 42 ";
const measurement_trimmed = measurement.trim();
const measurement_number = Number(measurement_trimmed);
const measurement_doubled = measurement_number * 2;
console.log(measurement_doubled); let measurement = " 42 ";
let measurement = measurement.trim(); // shadow: &str
let measurement: i32 = measurement.parse().unwrap(); // shadow: i32
let measurement = measurement * 2; // shadow: i32
println!("{}", measurement); Rust allows re-declaring a variable with the same name in the same scope — called shadowing. Each
let creates a new binding that hides the previous one. Unlike reassigning a let mut variable, shadowing can also change the type (as shown above: &str → i32). JavaScript has no equivalent because a second const or let declaration with the same name in the same scope is a syntax error. Shadowing is idiomatic in Rust for transformation chains that naturally reuse the same conceptual name.Ownership & Borrowing
Move semantics — ownership transfers on assignment
// JS: assignment copies the reference; both variables share the same object
const original = ["apple", "banana"];
const alias = original;
alias.push("cherry");
console.log(original); // ["apple", "banana", "cherry"] — same array!
console.log(alias); let original = vec!["apple", "banana"];
let moved = original; // ownership transfers to 'moved'
// println!("{:?}", original); // compile error: value was moved
println!("{:?}", moved); Rust's ownership model means that assigning a heap-allocated value (like a
Vec) to another variable moves ownership — the original variable can no longer be used. This is fundamentally different from JavaScript, where object assignment copies a reference so both variables share the same underlying data. Rust's move semantics prevent two places from owning the same heap memory, guaranteeing memory safety without a garbage collector. For stack-allocated types like integers, assignment copies the value automatically (they implement the Copy trait).clone() — explicit deep copy
// JS: spread creates a shallow copy — a new array, same elements
const original = ["apple", "banana"];
const copy = [...original];
copy.push("cherry");
console.log(original); // ["apple", "banana"] — unmodified
console.log(copy); // ["apple", "banana", "cherry"] let original = vec!["apple", "banana"];
let mut copy = original.clone(); // explicit deep copy — new allocation
copy.push("cherry");
println!("{:?}", original); // ["apple", "banana"] — unmodified
println!("{:?}", copy); // ["apple", "banana", "cherry"] Rust requires an explicit
.clone() call to deep-copy a heap-allocated value. Unlike JavaScript's spread ([...arr]) or Object.assign(), which are syntactic sugar, Rust's clone() makes the performance cost of copying visible in the source code — developers are always aware when an allocation occurs. For types like integers and booleans that implement the Copy trait, assignment automatically copies the value without any clone() call.Borrowing with & — shared references
// JS: function receives a reference automatically — "move" never happens
function measureLength(text) {
return text.length;
}
const greeting = "Hello, Rust!";
const length = measureLength(greeting);
console.log(greeting); // still accessible — JS refs are never consumed
console.log(length); fn measure_length(text: &String) -> usize {
text.len()
}
fn main() {
let greeting = String::from("Hello, Rust!");
let length = measure_length(&greeting); // borrow — does not move
println!("{}", greeting); // still accessible after the borrow
println!("{}", length);
} The
& prefix creates a reference (a borrow) — the function reads the value without taking ownership. JavaScript always passes objects by reference implicitly; Rust makes the reference explicit with & at both the call site (&greeting) and the parameter type (&String). After the function returns, ownership stays with the caller. Multiple immutable borrows can coexist simultaneously — Rust allows any number of &T references as long as no mutable reference exists at the same time.Mutable borrowing with &mut
// JS: any function can mutate an object — no mechanism prevents it
function addItem(items, item) {
items.push(item);
}
const shoppingList = ["apples"];
addItem(shoppingList, "bread");
console.log(shoppingList); // ["apples", "bread"] fn add_item<'a>(items: &mut Vec<&'a str>, item: &'a str) {
items.push(item);
}
fn main() {
let mut shopping_list = vec!["apples"];
add_item(&mut shopping_list, "bread");
println!("{:?}", shopping_list); // ["apples", "bread"]
} A mutable reference (
&mut) allows a function to modify the caller's value. Rust enforces an exclusive-access rule: at any given moment, a value may have either one mutable reference or any number of immutable references — never both simultaneously. This rule, enforced at compile time, eliminates data races entirely. In JavaScript, any function receiving an object can mutate it freely, with no mechanism to prevent accidental mutation — a common source of subtle bugs in larger codebases.Copy types — integers are automatically copied
// JS: primitive values are always copied
let first = 42;
let second = first;
second = 100;
console.log(first); // 42 — unchanged
console.log(second); // 100 let first = 42i32; // i32 implements Copy
let second = first; // copied, not moved — both remain valid
let result = second + 58;
println!("{}", first); // 42 — still valid
println!("{}", result); // 100 Rust types that are cheap to copy (integers, floats, booleans, chars, and fixed-size tuples of these) implement the
Copy trait. When a Copy type is assigned to another variable, the value is duplicated on the stack — ownership is not moved. This matches JavaScript's behavior for primitive values (number, boolean). Heap-allocated types like String and Vec do not implement Copy because copying them requires an allocation — use .clone() for an explicit copy, or & to borrow.Strings & Slices
String vs &str — owned vs borrowed
// JS has one string type — always immutable, always heap-allocated
const literal = "Hello";
let message = "World";
console.log(literal + ", " + message + "!"); let literal: &str = "Hello"; // &str: borrowed string slice
let owned: String = String::from("World"); // String: owned, heap-allocated
let combined = format!("{}, {}!", literal, owned);
println!("{}", combined); Rust has two string types:
&str (a string slice — a borrowed view of UTF-8 bytes, usually pointing into static memory or a String) and String (an owned, heap-allocated, growable string). JavaScript has a single string type that is always immutable. Function parameters usually accept &str because a &str can refer to both string literals and slices of String values, making it the more flexible choice for read-only string arguments.Creating and building strings
const name = "Alice";
const greeting = `Hello, ${name}!`;
console.log(greeting);
const parts = ["one", "two", "three"];
const joined = parts.join(", ");
console.log(joined); let name = "Alice";
let greeting = format!("Hello, {}!", name);
println!("{}", greeting);
let parts = vec!["one", "two", "three"];
let joined = parts.join(", ");
println!("{}", joined); Rust's
format!() macro is the equivalent of JavaScript's template literals — it creates a new String with interpolated values using {} as the placeholder (or {variable_name} in Rust's inline capture syntax). String concatenation with + in Rust works only for String + &str and is less ergonomic, so format!() is generally preferred. The .join() method on slices works exactly like JavaScript's Array.join().Common string methods
const text = " Hello, World! ";
console.log(text.trim());
console.log("hello".toUpperCase());
console.log("hello".includes("ell"));
console.log("hello".startsWith("hel"));
console.log("hello".repeat(3)); let text = " Hello, World! ";
println!("{}", text.trim());
println!("{}", "hello".to_uppercase());
println!("{}", "hello".contains("ell"));
println!("{}", "hello".starts_with("hel"));
println!("{}", "hello".repeat(3)); Rust's string methods closely mirror JavaScript's. The main naming differences are
to_uppercase() and to_lowercase() instead of toUpperCase() and toLowerCase(), and contains() instead of includes(). Rust strings are guaranteed to be valid UTF-8; indexing by byte position rather than character is a common source of beginner confusion. Use .chars().nth(n) for Unicode-correct character access rather than string[n].split() returns an iterator, not an array
const csv = "apple,banana,cherry";
const fruits = csv.split(",");
console.log(fruits); // ["apple", "banana", "cherry"]
console.log(fruits.length); // 3
console.log(fruits[1]); // banana let csv = "apple,banana,cherry";
let fruits: Vec<&str> = csv.split(',').collect(); // must collect to get a Vec
println!("{:?}", fruits); // ["apple", "banana", "cherry"]
println!("{}", fruits.len()); // 3
println!("{}", fruits[1]); // banana Rust's
.split() returns a lazy Split iterator rather than an array. To get a Vec, you must call .collect() at the end — the type annotation Vec<&str> tells collect() what to build. This design is intentional: if you only need to iterate over the parts once (e.g., in a for loop), allocating a full Vec is wasteful. The "lazy by default, collect when needed" pattern appears throughout Rust's iterator API.Formatting numbers and debug output
const pi = 3.14159265;
console.log(pi.toFixed(2)); // 3.14
const count = 42;
console.log(`There are ${count} items`);
const data = [1, 2, 3];
console.log(JSON.stringify(data)); // [1,2,3] let pi = 3.14159265f64;
println!("{:.2}", pi); // 3.14
let count = 42u32;
println!("There are {} items", count);
let data = vec![1, 2, 3];
println!("{:?}", data); // [1, 2, 3] — Debug format
println!("{:#?}", data); // pretty-printed Debug Rust's format specifiers go inside the curly braces:
{:.2} for two decimal places (like toFixed(2)), {:05} for zero-padded width, {:>10} for right-alignment. The {:?} Debug format produces output similar to JSON.stringify() for simple types and is available on any type that derives or implements the Debug trait. The {:#?} variant produces pretty-printed, indented output useful for nested data structures.Parsing strings to numbers
const parsed = parseInt("42", 10);
console.log(parsed + 1); // 43
const price = parseFloat("9.99");
console.log(price * 2); // 19.98
const bad = parseInt("abc", 10);
console.log(isNaN(bad)); // true — JS silently gives NaN let parsed: i32 = "42".parse().unwrap();
println!("{}", parsed + 1); // 43
let price: f64 = "9.99".parse().unwrap();
println!("{}", price * 2.0); // 19.98
// Safe parse — returns Result, not a silent NaN:
let result: Result<i32, _> = "abc".parse();
println!("{}", result.is_err()); // true Rust's
.parse() is generic — it returns a Result<T, E> that is either a successfully parsed value or a parse error. The type annotation on the binding (let parsed: i32) tells Rust what type to parse into. The .unwrap() call extracts the value and panics if parsing fails — convenient for examples, but production code handles the error with pattern matching or the ? operator. JavaScript's parseInt silently returns NaN on failure; Rust returns an Err variant that the type system forces you to handle.Collections
Vec<T> — Rust's dynamic array
const fruits = ["apple", "banana", "cherry"];
fruits.push("date");
console.log(fruits.length);
console.log(fruits[1]);
console.log(fruits); let mut fruits = vec!["apple", "banana", "cherry"];
fruits.push("date");
println!("{}", fruits.len());
println!("{}", fruits[1]);
println!("{:?}", fruits); Rust's
Vec<T> is the equivalent of a JavaScript array — a dynamically-sized, heap-allocated sequence. The vec! macro creates a Vec from literal values. push() adds to the end; pop() removes and returns the last element as an Option<T>. The .len() method replaces JavaScript's .length property. Indexing with [] panics if out of bounds; the safer .get(index) returns Option<&T> instead. A Vec must be declared mut to allow modification.HashMap — like JS Map
const scores = new Map([
["Alice", 95],
["Bob", 87],
]);
scores.set("Carol", 92);
console.log(scores.get("Alice")); // 95
console.log(scores.has("Dave")); // false
console.log(scores.size); // 3 use std::collections::HashMap;
let mut scores: HashMap<&str, u32> = HashMap::new();
scores.insert("Alice", 95);
scores.insert("Bob", 87);
scores.insert("Carol", 92);
println!("{:?}", scores.get("Alice")); // Some(95)
println!("{}", scores.contains_key("Dave")); // false
println!("{}", scores.len()); // 3 Rust's
HashMap<K, V> is the equivalent of JavaScript's Map. It requires use std::collections::HashMap. The .get(key) method returns Option<&V> — either Some(&value) or None — rather than returning undefined for missing keys as JavaScript does. This forces explicit handling of the missing-key case. The .entry(key).or_insert(default) pattern is idiomatic for "insert if absent, then modify," and is more efficient than a separate contains + insert because it performs only one hash lookup.entry().or_insert() — counting with a HashMap
const wordCounts = new Map();
const words = ["apple", "banana", "apple", "cherry", "banana", "apple"];
for (const word of words) {
wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);
}
for (const [word, count] of [...wordCounts.entries()].sort()) {
console.log(`${word}: ${count}`);
} use std::collections::HashMap;
let words = vec!["apple", "banana", "apple", "cherry", "banana", "apple"];
let mut word_counts: HashMap<&str, u32> = HashMap::new();
for &word in &words {
let count = word_counts.entry(word).or_insert(0);
*count += 1;
}
let mut pairs: Vec<_> = word_counts.iter().collect();
pairs.sort_by_key(|&(word, _)| *word);
for (word, count) in pairs {
println!("{}: {}", word, count);
} The
.entry(key).or_insert(default) pattern inserts a default value if the key is absent, then returns a mutable reference to the stored value. Modifying the value requires dereferencing with *count += 1 because or_insert returns a &mut V. This single-call pattern is more efficient than JavaScript's (map.get(key) ?? 0) + 1 followed by map.set(key, newValue), because it performs only one hash lookup instead of two.Vec operations — contains, sort, dedup
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5];
console.log(numbers.includes(5)); // true
const sorted = [...numbers].sort((a, b) => a - b);
console.log(sorted);
const unique = [...new Set(sorted)];
console.log(unique); let numbers = vec![3, 1, 4, 1, 5, 9, 2, 6, 5];
println!("{}", numbers.contains(&5)); // true
let mut sorted = numbers.clone();
sorted.sort();
println!("{:?}", sorted);
sorted.dedup(); // removes consecutive duplicates (works correctly after sort)
println!("{:?}", sorted); Rust's
Vec has .contains(&value) (note the &), .sort() (in-place), .dedup() (removes consecutive duplicate elements — call after sorting for uniqueness), and .retain(predicate) for in-place filtering. JavaScript's .sort() mutates the original array by default; Rust's .sort() also mutates in place, which is why sorted = numbers.clone() is used above to preserve the original. The Set uniqueness pattern in JavaScript corresponds to sorting and then calling .dedup() in Rust.Ranges — a first-class language feature
// JS has no built-in range — use Array.from:
const oneToFive = Array.from({ length: 5 }, (_, i) => i + 1);
console.log(oneToFive);
const evens = Array.from({ length: 5 }, (_, i) => (i + 1) * 2);
console.log(evens); // 1..=5 is inclusive; 0..5 is exclusive-end
let one_to_five: Vec<i32> = (1..=5).collect();
println!("{:?}", one_to_five);
let zero_to_four: Vec<i32> = (0..5).collect();
println!("{:?}", zero_to_four);
let evens: Vec<i32> = (2..=10).step_by(2).collect();
println!("{:?}", evens);
println!("{}", (1..=5).contains(&3)); // true Rust has first-class range syntax:
1..5 is exclusive-end (1, 2, 3, 4) and 1..=5 is inclusive (1, 2, 3, 4, 5). Ranges are lazy iterators — they allocate no memory until consumed with .collect() or a for loop, so 1..1_000_000 is cheap to create. JavaScript has no built-in range syntax; the common workaround allocates a full array immediately. Ranges also work as slice indices: &vec[1..3] gives a view of elements 1 and 2.Const generics
function sum(values) {
return values.reduce((total, value) => total + value, 0);
}
// JS arrays are dynamic; length is never part of a "type".
console.log(sum([1, 2, 3]));
console.log(sum([1, 2, 3, 4, 5])); // N is a compile-time constant: each array length is its own type,
// yet one definition covers them all.
fn sum<const N: usize>(values: [i32; N]) -> i32 {
values.iter().sum()
}
fn main() {
println!("{}", sum([1, 2, 3]));
println!("{}", sum([1, 2, 3, 4, 5]));
} Const generics let a definition be parameterized over a value — here the array length
N — not just over types. JavaScript has no such notion: every array is dynamically sized and length is a runtime property. Because the length is part of the Rust type and known at compile time, [i32; 3] and [i32; 5] are distinct types served by one sum, stored inline with no heap allocation. (For a growable, JS-array-like sequence you would reach for Vec<T> instead.)Control Flow
if as an expression — no ternary operator
const score = 75;
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
const result = score >= 70 ? "pass" : "fail";
console.log(grade);
console.log(result); let score = 75u32;
// if is an expression — no ternary ? : operator in Rust
let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else if score >= 70 { "C" } else { "F" };
let result = if score >= 70 { "pass" } else { "fail" };
println!("{}", grade);
println!("{}", result); In Rust,
if/else is an expression that produces a value — there is no separate ternary operator ? :. All branches must produce the same type; the compiler enforces this. The else branch is required whenever the if expression is used as a value, because the compiler must guarantee a value for every code path. The last expression in each block is its return value — no explicit return keyword is needed.while, for…in, and bare loop
let countdown = 3;
while (countdown > 0) {
console.log(countdown);
countdown--;
}
const colors = ["red", "green", "blue"];
for (const color of colors) {
console.log(color);
} let mut countdown = 3u32;
while countdown > 0 {
println!("{}", countdown);
countdown -= 1;
}
let colors = vec!["red", "green", "blue"];
for color in &colors {
println!("{}", color);
} Rust's
while loop is syntactically identical to JavaScript's, without parentheses. Rust's for item in collection corresponds to JavaScript's for (const item of collection). Iterating with for color in &colors borrows the Vec (leaving colors accessible afterwards); for color in colors moves the Vec into the loop, consuming it. Rust also has a bare loop { } that runs forever until a break, which is idiomatic for retry loops and state machines.loop with break — returning a value
let multiplier = 1;
// JS: use IIFE to return a value from a loop
const result = (() => {
while (true) {
if (multiplier * 7 > 50) return multiplier * 7;
multiplier++;
}
})();
console.log(result); let mut multiplier = 1u32;
// Rust: break can carry a value out of loop
let result = loop {
if multiplier * 7 > 50 {
break multiplier * 7;
}
multiplier += 1;
};
println!("{}", result); Rust's
loop is unique: break value causes the entire loop expression to evaluate to that value. This allows loops to be used as expressions, avoiding the need for a mutable outer variable that is set inside the loop and read outside. In JavaScript, the closest equivalent is an immediately-invoked function expression (IIFE) with an early return — Rust's loop { break value } is cleaner and does not require a new function scope or closure allocation.Range-based for loops and enumerate
for (let i = 1; i <= 5; i++) {
console.log(i);
}
const fruits = ["apple", "banana", "cherry"];
fruits.forEach((fruit, index) => {
console.log(`${index}: ${fruit}`);
}); for i in 1..=5 {
println!("{}", i);
}
let fruits = vec!["apple", "banana", "cherry"];
for (index, fruit) in fruits.iter().enumerate() {
println!("{}: {}", index, fruit);
} Rust's range-based
for loop is the idiomatic replacement for JavaScript's C-style for (let i = 0; i < n; i++). The 1..=5 range includes both endpoints; 1..6 excludes the end. For indexed iteration, .enumerate() produces (index, &value) tuples — analogous to JavaScript's .forEach((value, index) => { }), but note that Rust's pair has the index first, then the value.Pattern Matching
match — exhaustive switch without fall-through
const day = "Monday";
let kind;
switch (day) {
case "Saturday": case "Sunday":
kind = "weekend"; break;
case "Monday": case "Friday":
kind = "busy"; break;
default:
kind = "regular";
}
console.log(kind); let day = "Monday";
let kind = match day {
"Saturday" | "Sunday" => "weekend",
"Monday" | "Friday" => "busy",
_ => "regular",
};
println!("{}", kind); Rust's
match expression replaces JavaScript's switch statement, with key improvements: there is no fall-through between arms (no break needed), match is an expression that returns a value, and the compiler enforces exhaustiveness — forgetting to handle a case is a compile error. Multiple patterns in one arm are separated with |. The _ wildcard is the catch-all, equivalent to JavaScript's default.Match guards — conditions inside arms
const temperature = 25;
let description;
if (temperature < 0) description = "freezing";
else if (temperature < 15) description = "cold";
else if (temperature < 25) description = "mild";
else description = "warm";
console.log(description); let temperature = 25i32;
let description = match temperature {
t if t < 0 => "freezing",
t if t < 15 => "cold",
t if t < 25 => "mild",
_ => "warm",
};
println!("{}", description); Match arms can include a guard condition (
if condition after the pattern), allowing arbitrary boolean expressions. The guard receives a binding of the matched value (t in this example). A match with guards behaves like an if-else if chain but integrates cleanly with Rust's pattern matching — matching on type, destructuring, and guarding can all appear in the same arm. The final _ wildcard covers all cases that no guard matches.Destructuring in patterns
const [first, second] = [1, 2, 3];
console.log(first, second);
// Destructuring in a condition:
const coordinate = [3, -5];
if (coordinate[0] === 0 || coordinate[1] === 0) {
console.log("on an axis");
} else {
console.log(`at (${coordinate[0]}, ${coordinate[1]})`);
} let (first, second) = (1, 2);
println!("{} {}", first, second);
// Structural match — patterns test values, not just bind:
let coordinate = (3i32, -5i32);
match coordinate {
(0, 0) => println!("at the origin"),
(x, 0) | (0, x) => println!("on an axis at {}", x),
(x, y) => println!("at ({}, {})", x, y),
} Rust's pattern matching supports destructuring of tuples, structs, enums, and references directly in
let bindings and match arms. Unlike JavaScript destructuring (which always binds all specified positions regardless of values), Rust patterns can test values simultaneously. The pattern (x, 0) matches only when the second element is exactly 0 and binds the first to x — combining a structural test and a binding in one step.if let — pattern matching one case
const maybeValue = 42; // could also be null or undefined
if (maybeValue != null) {
console.log(`Got value: ${maybeValue}`);
} else {
console.log("No value");
} let maybe_value: Option<i32> = Some(42);
if let Some(value) = maybe_value {
println!("Got value: {}", value);
} else {
println!("No value");
}
// Works for any pattern, not just Option:
let pair = (true, 7u32);
if let (true, count) = pair {
println!("Active with count {}", count);
} if let combines a pattern match and a conditional — it runs the body only when the value matches the pattern, binding any names in the pattern. It is shorthand for a two-arm match where the second arm does nothing. if let Some(value) = maybe is the idiomatic way to unwrap an Option when only the Some case matters. JavaScript's null-check (if (value != null)) does not produce a new binding; Rust's if let produces a binding guaranteed to hold a non-null value inside the block.while let — loop until a pattern fails
const stack = [1, 2, 3, 4, 5];
while (stack.length > 0) {
const item = stack.pop();
console.log(`Processing: ${item}`);
} let mut stack = vec![1, 2, 3, 4, 5];
while let Some(item) = stack.pop() {
println!("Processing: {}", item);
} while let loops as long as the value matches the pattern and stops when it does not. Vec::pop() returns Option<T> — Some(value) when the Vec is non-empty, None when empty. The while let Some(item) = stack.pop() loop processes items one by one until the Vec is empty, with item bound to each value in turn. This is more expressive than JavaScript's while (arr.length > 0) { const item = arr.pop(); } because the pop and the emptiness check are unified in a single pattern.if-let chains
const settings = new Map([["timeout", 30]]);
const value = settings.get("timeout");
if (value !== undefined && value > 10) {
console.log(`long timeout: ${value}`);
} use std::collections::HashMap;
fn main() {
let settings: HashMap<&str, i32> =
[("timeout", 30)].into_iter().collect();
// Edition 2024: chain a let pattern and a bool test with &&
if let Some(&value) = settings.get("timeout") && value > 10 {
println!("long timeout: {value}");
}
} Stabilised in the 2024 edition, let chains let a
let pattern and ordinary boolean tests be joined with && inside one if — and crucially the binding from the let link is in scope for the tests that follow. That makes the Rust form tighter than the JavaScript one, which needs a separate const value = ... before the if so the comparison can see it. The Some(..) pattern also distinguishes "absent" from "present", where JS leans on an undefined check. Before 2024 this needed a nested if let { if value > 10 { ... } }.Functions & Closures
Function declaration — explicit types required
function greet(name) {
return `Hello, ${name}!`;
}
function square(number) {
return number * number;
}
console.log(greet("World"));
console.log(square(5)); fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
fn square(number: i32) -> i32 {
number * number // last expression is the return value — no semicolon
}
fn main() {
println!("{}", greet("World"));
println!("{}", square(5));
} Rust functions use the
fn keyword with required type annotations on all parameters and the return type (after ->). The last expression in a function body is implicitly returned — no return keyword needed (though return is valid for early exits). A semicolon on the last line makes it a statement that returns () (unit/void), causing a type error if a non-unit return type is declared. Functions default to private visibility; add pub to export from a module.Closures — |params| instead of arrow
const multiply = (x, y) => x * y;
const double = x => x * 2;
const greet = name => `Hello, ${name}!`;
console.log(multiply(3, 7));
console.log(double(5));
console.log(greet("Rust")); let multiply = |x: i32, y: i32| x * y;
let double = |x: i32| x * 2;
let greet = |name: &str| format!("Hello, {}!", name);
println!("{}", multiply(3, 7));
println!("{}", double(5));
println!("{}", greet("Rust")); Rust closures use pipes
|params| instead of JavaScript's arrow =>. When the body is a single expression, curly braces are optional and the expression is implicitly returned — analogous to JavaScript's implicit-return arrow x => x * 2. Rust can often infer closure parameter types from context, so type annotations are frequently omitted. Unlike JavaScript, where all closures capture by reference automatically, Rust closures capture by the most restrictive mode possible: by reference, by mutable reference, or by move.Higher-order functions — typed function parameters
function applyTwice(fn, value) {
return fn(fn(value));
}
const triple = x => x * 3;
console.log(applyTwice(triple, 2)); // 18 fn apply_twice<F: Fn(i32) -> i32>(operation: F, value: i32) -> i32 {
operation(operation(value))
}
fn main() {
let triple = |x| x * 3;
println!("{}", apply_twice(triple, 2)); // 18
// Inline closure also works:
println!("{}", apply_twice(|x| x + 10, 5)); // 25
} Rust function types are written as trait bounds:
Fn(T) -> R for an immutable closure, FnMut(T) -> R for one that mutates captured variables, and FnOnce(T) -> R for one that consumes captured values. The <F: Fn(i32) -> i32> syntax declares a generic function that accepts any callable with that signature. JavaScript treats functions as untyped values — calling a non-function silently throws at runtime. Rust catches type mismatches at compile time, without any opt-in.move closures — taking ownership of captured values
function makeAdder(addend) {
return x => x + addend; // captures addend from outer scope
}
const addFive = makeAdder(5);
console.log(addFive(10)); // 15
console.log(addFive(20)); // 25 fn make_adder(addend: i32) -> impl Fn(i32) -> i32 {
move |x| x + addend // 'move' takes ownership of addend into the closure
}
fn main() {
let add_five = make_adder(5);
println!("{}", add_five(10)); // 15
println!("{}", add_five(20)); // 25
} The
move keyword before a closure forces it to take ownership of all captured variables. This is necessary when the closure outlives the scope where the captured variables were defined — for example, when returning a closure from a function or sending one to another thread. In JavaScript, closures always capture the surrounding scope by reference implicitly, keeping captured variables alive; Rust's move is the explicit equivalent that transfers ownership. The impl Fn(i32) -> i32 return type says "some type that implements Fn" without naming the concrete closure type.No default parameters — use Option or overloading
function createGreeting(name, prefix = "Hello", punctuation = "!") {
return `${prefix}, ${name}${punctuation}`;
}
console.log(createGreeting("Alice"));
console.log(createGreeting("Bob", "Hi"));
console.log(createGreeting("Carol", "Hey", ".")); fn create_greeting(name: &str, prefix: Option<&str>, punctuation: Option<&str>) -> String {
let prefix = prefix.unwrap_or("Hello");
let punctuation = punctuation.unwrap_or("!");
format!("{}, {}{}", prefix, name, punctuation)
}
fn main() {
println!("{}", create_greeting("Alice", None, None));
println!("{}", create_greeting("Bob", Some("Hi"), None));
println!("{}", create_greeting("Carol", Some("Hey"), Some(".")));
} Rust does not have default parameter values. The common idiom is to accept
Option<T> for optional parameters and use .unwrap_or(default) to supply the default inside the function. Another pattern is a builder struct (for many optional fields) or separate functions with descriptive names. While more verbose than JavaScript's param = default syntax, the Option approach makes the optionality visible in the type signature, which is especially useful for library APIs.Structs & Methods
Structs — no class keyword
class Point {
constructor(x, y) { this.x = x; this.y = y; }
}
const origin = new Point(0, 0);
const target = new Point(3, 4);
console.log(origin.x, origin.y);
console.log(target.x, target.y); struct Point {
x: f64,
y: f64,
}
fn main() {
let origin = Point { x: 0.0, y: 0.0 }; // no 'new' keyword
let target = Point { x: 3.0, y: 4.0 };
println!("{} {}", origin.x, origin.y);
println!("{} {}", target.x, target.y);
} Rust has no
class keyword. Data is defined in a struct, and behavior is added separately in an impl block. Struct fields are accessed with dot notation, identical to JavaScript class instances. Initialization uses FieldName: value syntax — all fields must be provided (there are no default field values unless you implement the Default trait). There is no new keyword; constructors are associated functions conventionally named new but treated no differently from any other function.impl blocks — methods on structs
class Rectangle {
constructor(width, height) {
this.width = width; this.height = height;
}
area() { return this.width * this.height; }
isSquare() { return this.width === this.height; }
perimeter() { return 2 * (this.width + this.height); }
}
const rectangle = new Rectangle(4, 6);
console.log(rectangle.area(), rectangle.isSquare(), rectangle.perimeter()); struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
fn area(&self) -> f64 { self.width * self.height }
fn is_square(&self) -> bool { self.width == self.height }
fn perimeter(&self) -> f64 { 2.0 * (self.width + self.height) }
}
fn main() {
let rectangle = Rectangle { width: 4.0, height: 6.0 };
println!("{} {} {}", rectangle.area(), rectangle.is_square(), rectangle.perimeter());
} Methods are defined in an
impl block attached to a struct. The first parameter &self is the receiver — analogous to JavaScript's this, but explicit and typed. &self borrows the struct immutably; &mut self borrows it mutably (required for methods that modify fields); plain self consumes it. A struct can have multiple impl blocks, which is useful for organizing methods or adding trait implementations separately.Associated functions — like static methods
class User {
constructor(name, email) {
this.name = name; this.email = email;
}
static create(name, email) { return new User(name, email); }
static guest() { return new User("Guest", "guest@example.com"); }
}
const user = User.create("Alice", "alice@example.com");
const guest = User.guest();
console.log(user.name, guest.name); struct User {
name: String,
email: String,
}
impl User {
fn create(name: &str, email: &str) -> User {
User { name: name.to_string(), email: email.to_string() }
}
fn guest() -> User { User::create("Guest", "guest@example.com") }
fn display(&self) { println!("{} <{}>", self.name, self.email); }
}
fn main() {
let user = User::create("Alice", "alice@example.com");
let guest = User::guest();
user.display();
guest.display();
} Functions in an
impl block that do not take self as a parameter are called associated functions — the equivalent of JavaScript's static methods. They are called with double-colon syntax (User::create()). The conventional name for a constructor-style associated function is new, though Rust has no special-casing for it. JavaScript's new MyClass() calls the constructor; Rust's MyStruct::new() calls an ordinary associated function that returns a value of the struct type.#[derive] — Debug, Clone, PartialEq for free
class Point {
constructor(x, y) { this.x = x; this.y = y; }
toString() { return `Point(${this.x}, ${this.y})`; }
equals(other) { return this.x === other.x && this.y === other.y; }
}
const pointA = new Point(1, 2);
const pointB = new Point(1, 2);
console.log(pointA.toString());
console.log(pointA.equals(pointB)); #[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point_a = Point { x: 1, y: 2 };
let point_b = point_a.clone();
let point_c = Point { x: 3, y: 4 };
println!("{:?}", point_a); // Point { x: 1, y: 2 }
println!("{}", point_a == point_b); // true
println!("{}", point_a == point_c); // false
} The
#[derive(...)] attribute automatically generates implementations of common traits: Debug enables {:?} printing (like JSON.stringify()), Clone enables .clone(), and PartialEq enables == and != based on field values. Rust's == on custom types uses structural equality only when PartialEq is derived or implemented — without it, the compiler refuses to compile ==. In JavaScript, === always compares objects by reference identity, not by value.Struct update syntax — like object spread
const alice = { name: "Alice", age: 30, role: "admin" };
const updated = { ...alice, age: 31 }; // spread then override
console.log(updated); #[derive(Debug)]
struct Profile {
name: String,
age: u32,
role: String,
}
fn main() {
let alice = Profile {
name: String::from("Alice"),
age: 30,
role: String::from("admin"),
};
let profile_b = Profile { age: 31, ..alice }; // ..alice fills the rest
println!("{:?}", profile_b);
} Rust's struct update syntax (
..existing) copies the remaining fields from an existing instance — similar to JavaScript's object spread ({ ...obj, overrides }). One important difference: the update syntax moves non-Copy fields (like String) out of the original struct, so the original becomes partially moved and inaccessible afterward. To avoid this, clone the original first. Fields listed before .. take precedence over the copied fields.Traits
Defining and implementing traits
// JS: no interface keyword in plain JS — duck typing
class Circle {
constructor(radius) { this.radius = radius; }
area() { return Math.PI * this.radius ** 2; }
perimeter() { return 2 * Math.PI * this.radius; }
}
const circle = new Circle(5);
console.log(circle.area().toFixed(2)); use std::f64::consts::PI;
trait Shape {
fn area(&self) -> f64;
fn perimeter(&self) -> f64;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { PI * self.radius * self.radius }
fn perimeter(&self) -> f64 { 2.0 * PI * self.radius }
}
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
fn perimeter(&self) -> f64 { 4.0 * self.side }
}
fn main() {
let circle = Circle { radius: 5.0 };
let square = Square { side: 4.0 };
println!("{:.2}", circle.area());
println!("{:.2}", square.perimeter());
} Rust traits define a set of methods a type must implement — similar to TypeScript interfaces. Unlike JavaScript (which has no static interface mechanism), Rust traits are enforced at compile time: implementing a trait incorrectly is a compile error. A struct can implement multiple traits. Trait methods that are missing from an
impl block cause the compiler to list exactly which methods need to be added. Traits can also provide default method implementations, allowing partial reuse without a class hierarchy.Default trait methods — reuse without inheritance
class Logger {
log(message) { console.log(`[INFO] ${message}`); }
warn(message) { this.log(`WARNING: ${message}`); }
error(message) { this.log(`ERROR: ${message}`); }
}
const logger = new Logger();
logger.warn("Something looks off");
logger.error("Disk full"); trait Logger {
fn log(&self, message: &str); // required — must be implemented
fn warn(&self, message: &str) { self.log(&format!("WARNING: {}", message)); }
fn error(&self, message: &str) { self.log(&format!("ERROR: {}", message)); }
}
struct ConsoleLogger;
impl Logger for ConsoleLogger {
fn log(&self, message: &str) { println!("[INFO] {}", message); }
// warn() and error() use the default implementations from the trait
}
fn main() {
let logger = ConsoleLogger;
logger.warn("Something looks off");
logger.error("Disk full");
} Trait methods can have default implementations that are inherited automatically by types that do not override them. Only the required methods (those without a body) must be provided in each
impl block. Default methods can call other trait methods, including required ones — the warn and error defaults both delegate to self.log(), which each implementor supplies. This is the primary mechanism for code reuse in Rust, replacing class inheritance.Display trait — custom {} formatting
class Color {
constructor(red, green, blue) {
this.red = red; this.green = green; this.blue = blue;
}
toString() { return `rgb(${this.red}, ${this.green}, ${this.blue})`; }
}
const color = new Color(255, 128, 0);
console.log(String(color));
console.log(`The color is: ${color}`); use std::fmt;
struct Color { red: u8, green: u8, blue: u8 }
impl fmt::Display for Color {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "rgb({}, {}, {})", self.red, self.green, self.blue)
}
}
fn main() {
let color = Color { red: 255, green: 128, blue: 0 };
println!("{}", color);
println!("The color is: {}", color);
} Implementing
std::fmt::Display for a custom type enables {} formatting and automatic .to_string() — the equivalent of implementing toString() in JavaScript. The fmt method writes formatted output into a Formatter using write!. Once Display is implemented, the type works in all format strings, in println!, and anywhere a human-readable string representation is expected.dyn Trait — runtime polymorphism
// JS: duck typing — any object with the right method works
function printArea(shape) {
console.log(`Area: ${shape.area().toFixed(2)}`);
}
printArea({ area: () => Math.PI * 25 }); // circle-like
printArea({ area: () => 16 }); // square-like use std::f64::consts::PI;
trait Shape { fn area(&self) -> f64; }
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle { fn area(&self) -> f64 { PI * self.radius * self.radius } }
impl Shape for Square { fn area(&self) -> f64 { self.side * self.side } }
fn print_area(shape: &dyn Shape) {
println!("Area: {:.2}", shape.area());
}
fn main() {
print_area(&Circle { radius: 5.0 });
print_area(&Square { side: 4.0 });
// Heterogeneous collection of shapes:
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 3.0 }),
Box::new(Square { side: 2.0 }),
];
for shape in &shapes { print_area(shape.as_ref()); }
} dyn Trait creates a trait object — a pointer to a value of any type implementing the trait, with method dispatch resolved at runtime. This is Rust's equivalent of JavaScript's duck typing. The alternative, generics (<S: Shape>), resolves at compile time (monomorphization) and generates separate machine code per concrete type, which is faster but increases binary size. Trait objects are the right choice when the concrete type is not known at compile time, such as in a mixed collection of shapes. Box<dyn Trait> heap-allocates the value so its size is uniform.Enums & Option
Enums with associated data
// JS: tagged union pattern
function makeShape(type, ...args) {
if (type === "circle") return { type, radius: args[0] };
if (type === "rectangle") return { type, width: args[0], height: args[1] };
}
const shapes = [makeShape("circle", 5), makeShape("rectangle", 3, 4)];
for (const shape of shapes) {
if (shape.type === "circle") console.log(`Circle area: ${(Math.PI * shape.radius ** 2).toFixed(2)}`);
if (shape.type === "rectangle") console.log(`Rectangle area: ${shape.width * shape.height}`);
} use std::f64::consts::PI;
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
}
}
}
fn main() {
let shapes = vec![
Shape::Circle { radius: 5.0 },
Shape::Rectangle { width: 3.0, height: 4.0 },
];
for shape in &shapes {
println!("{:.2}", shape.area());
}
} Rust enums can carry data in each variant — a feature absent from JavaScript's
Object.freeze() enum pattern. The enum Shape cleanly expresses "a Shape is either a Circle with a radius, or a Rectangle with a width and height." Pattern matching destructures the variant's data simultaneously with the type check, and the compiler enforces exhaustiveness — forgetting to handle a variant is a compile error. This replaces JavaScript's tagged union (if (obj.type === "circle")) with a type-safe, exhaustive mechanism.Option<T> — no null or undefined
function findUser(identifier) {
const users = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
return users.find(user => user.id === identifier); // undefined if not found
}
const found = findUser(1);
const missing = findUser(99);
console.log(found?.name ?? "not found");
console.log(missing?.name ?? "not found"); fn find_user(identifier: u32) -> Option<&'static str> {
let users = vec![(1u32, "Alice"), (2u32, "Bob")];
users.iter()
.find(|(user_id, _)| *user_id == identifier)
.map(|(_, name)| *name)
}
fn main() {
println!("{}", find_user(1).unwrap_or("not found"));
println!("{}", find_user(99).unwrap_or("not found"));
} Rust's
Option<T> is an enum with two variants: Some(value) for a present value and None for absence — there is no null or undefined. A function returning Option<String> communicates to callers that the result may be absent, and the type system forces them to handle both cases. This eliminates the JavaScript "billion-dollar mistake" of null silently propagating through code. JavaScript's optional chaining (?.) and nullish coalescing (??) exist precisely to work around the implicit nullability of every value.Option methods — map, and_then, unwrap_or
// Chain of transformations — each step might produce null
const doubled = "42" !== null
? parseInt("42", 10) * 2
: null;
console.log(doubled); // 84
const result = (null !== null ? parseInt(null, 10) * 2 : null) ?? 0;
console.log(result); // 0 let doubled: Option<i32> = Some("42")
.and_then(|text| text.parse::<i32>().ok()) // None if parse fails
.map(|number| number * 2);
println!("{:?}", doubled); // Some(84)
let absent: Option<&str> = None;
let result = absent
.and_then(|text| text.parse::<i32>().ok())
.map(|number| number * 2)
.unwrap_or(0);
println!("{}", result); // 0 Option<T> has a rich set of methods for chaining transformations without explicit null checks. .map(fn) applies a function to the inner value if Some, leaving None unchanged. .and_then(fn) is "flat map" — the function returns an Option, preventing Some(Some(x)) double-wrapping. .unwrap_or(default) extracts the value or returns a default, equivalent to JavaScript's ?? default. These methods enable chaining operations on nullable values without nested null checks, but with the type system enforcing every possible absent-value case.Methods on enums
const LIGHTS = { red: "red", yellow: "yellow", green: "green" };
function duration(color) {
if (color === LIGHTS.red) return 60;
if (color === LIGHTS.yellow) return 5;
return 45;
}
function next(color) {
if (color === LIGHTS.red) return LIGHTS.green;
if (color === LIGHTS.green) return LIGHTS.yellow;
return LIGHTS.red;
}
console.log(duration(LIGHTS.red));
console.log(next(LIGHTS.red)); #[derive(Debug)]
enum TrafficLight { Red, Yellow, Green }
impl TrafficLight {
fn duration(&self) -> u32 {
match self {
TrafficLight::Red => 60,
TrafficLight::Yellow => 5,
TrafficLight::Green => 45,
}
}
fn next(&self) -> TrafficLight {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Green => TrafficLight::Yellow,
TrafficLight::Yellow => TrafficLight::Red,
}
}
}
fn main() {
let light = TrafficLight::Red;
println!("{}", light.duration());
println!("{:?}", light.next());
} In Rust, enums can have
impl blocks just like structs — methods are added to the enum type directly. The match self { ... } pattern inside an enum method handles every variant, and the compiler enforces exhaustiveness. Adding a new variant to the Rust enum immediately causes compile errors in every match that does not handle it, making refactoring safe. This is a significant advantage over JavaScript's equivalent chain of if/else if checks on string values.Error Handling
Result<T, E> — errors in the type system
function parseTemperature(input) {
const value = parseFloat(input);
if (isNaN(value)) throw new Error(`"${input}" is not a valid temperature`);
return value;
}
try {
console.log(parseTemperature("22.5"));
console.log(parseTemperature("hot"));
} catch (error) {
console.log(`Error: ${error.message}`);
} fn parse_temperature(input: &str) -> Result<f64, String> {
input.parse::<f64>()
.map_err(|_| format!("invalid temperature value: {}", input))
}
fn main() {
match parse_temperature("22.5") {
Ok(temp) => println!("{}", temp),
Err(message) => println!("Error: {}", message),
}
match parse_temperature("hot") {
Ok(temp) => println!("{}", temp),
Err(message) => println!("Error: {}", message),
}
} Rust's
Result<T, E> enum encodes either a successful value (Ok(T)) or an error (Err(E)) in the return type itself — the type system forces callers to acknowledge that the function can fail. JavaScript's throw/catch mechanism allows errors to propagate silently through any number of call stack layers without appearing in function signatures. A caller can call a throwing JavaScript function without any try/catch and the compiler will not warn them. In Rust, ignoring a Result produces a compiler warning, and using the value without handling the error case is a type error.? operator — propagate errors without boilerplate
// JS: every await can throw; callers must use try/catch or .catch()
async function processInput(input) {
const trimmed = input.trim();
if (!trimmed) throw new Error("empty input");
const number = parseInt(trimmed, 10);
if (isNaN(number)) throw new Error(`"${trimmed}" is not a number`);
return number * 2;
} fn process_input(input: &str) -> Result<i32, String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err("empty input".to_string());
}
let number: i32 = trimmed.parse()
.map_err(|_| format!("invalid number: {}", trimmed))?; // ? propagates Err
Ok(number * 2)
}
fn main() {
println!("{:?}", process_input(" 21 ")); // Ok(42)
println!("{:?}", process_input(" abc ")); // Err("invalid number: abc")
println!("{:?}", process_input(" ")); // Err("empty input")
} The
? operator short-circuits on Err — it is syntactic sugar for returning early with the error if the value is Err, or unwrapping the Ok value to continue. A function using ? must return Result (or Option). This makes error propagation explicit and visible: a line with ? is a clear "this can fail and return early" marker, unlike JavaScript's await where any awaited expression can throw without any visual indicator at the call site.panic! — unrecoverable errors vs safe access
const numbers = [1, 2, 3];
try {
console.log(numbers[10]); // JS: undefined, not an error
console.log(numbers.at(10)); // undefined
} catch (error) {
console.log(error.message);
}
// Explicit range check:
const index = 1;
if (index >= 0 && index < numbers.length) {
console.log(numbers[index]);
} let numbers = vec![1, 2, 3];
// Direct index: panics on out-of-bounds — like an uncaught JS exception
// let value = numbers[10]; // would panic with index out of bounds
// .get() returns Option — safe, never panics:
println!("{:?}", numbers.get(1)); // Some(2)
println!("{:?}", numbers.get(10)); // None
// Check and access:
if let Some(value) = numbers.get(1) {
println!("{}", value);
} A Rust
panic! terminates the current thread with a message and backtrace — similar to an uncaught exception that crashes the process. Direct Vec indexing with [] panics on out-of-bounds, whereas JavaScript returns undefined silently. The safe alternative is .get(index), which returns Option<&T>. The Rust philosophy: use Result for expected failure modes (user input errors, missing files) and panic! for logic bugs that indicate the program is in an invalid state it was never designed to handle.Custom error types
class AppError extends Error {
constructor(kind, message) {
super(message);
this.kind = kind;
}
toString() { return `[${this.kind}] ${this.message}`; }
}
const error = new AppError("NotFound", "user #42 does not exist");
console.log(String(error));
console.log(error instanceof Error); #[derive(Debug)]
enum AppError {
NotFound(String),
ParseError(String),
NetworkError { code: u16, message: String },
}
impl std::fmt::Display for AppError {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::NotFound(resource) => write!(formatter, "[NotFound] {}", resource),
AppError::ParseError(details) => write!(formatter, "[ParseError] {}", details),
AppError::NetworkError { code, message } => write!(formatter, "[Network {}] {}", code, message),
}
}
}
fn main() {
let error = AppError::NotFound("user #42 does not exist".to_string());
println!("{}", error);
println!("{:?}", error);
} Custom error types in Rust are typically enums with variants for each failure mode, carrying different data per variant rather than relying on a class hierarchy. Implementing
std::fmt::Display provides human-readable messages; #[derive(Debug)] provides diagnostic output. The combination gives the type full formatting support. Implementing std::error::Error marks it as an error type, enabling interoperability with error-handling libraries like anyhow and thiserror from the Rust ecosystem.Result methods — map, map_err, unwrap_or
function parsePositiveInt(input) {
const number = parseInt(input.trim(), 10);
if (isNaN(number)) return { ok: false, error: `"${input.trim()}" is not a number` };
if (number <= 0) return { ok: false, error: "must be positive" };
return { ok: true, value: number };
}
console.log(parsePositiveInt(" 42 "));
console.log(parsePositiveInt(" -5 ")); fn parse_positive_int(input: &str) -> Result<u32, String> {
let trimmed = input.trim();
trimmed.parse::<u32>()
.map_err(|_| format!("invalid positive integer: {}", trimmed))
}
fn main() {
println!("{:?}", parse_positive_int(" 42 ")); // Ok(42)
println!("{:?}", parse_positive_int(" -5 ")); // Err(...)
println!("{:?}", parse_positive_int(" abc ")); // Err(...)
let value = parse_positive_int(" 21 ").unwrap_or(0);
println!("{}", value); // 21
} Result<T, E> has methods mirroring those of Option<T>: .map(fn) transforms the Ok value if present; .map_err(fn) transforms the Err value; .and_then(fn) chains a fallible operation; .unwrap_or(default) extracts Ok or returns a default. Note that parsing to u32 automatically rejects negative inputs (since unsigned integers cannot be negative), cleanly expressing "must be a non-negative integer" at the type level. This is a small example of how Rust's type system can encode constraints that would require manual validation in JavaScript.let-else early return
function parseAndDouble(text) {
const number = Number(text);
if (Number.isNaN(number)) {
console.log("not a number");
return;
}
console.log(number * 2);
}
parseAndDouble("42"); fn parse_and_double(text: &str) {
let Ok(number) = text.parse::<i32>() else {
println!("not a number");
return;
};
println!("{}", number * 2);
}
fn main() {
parse_and_double("42");
} A
let ... else binding is Rust's built-in version of the JavaScript "guard and bail" pattern (if (Number.isNaN(number)) return;). It pattern-matches on the success value and binds it in the surrounding scope; if the match fails it runs the else block, which must diverge (return, break, continue, or panic!). Where JS's Number("abc") silently yields NaN and hopes you check, Rust's parse returns a Result the compiler forces you to handle, and let-else keeps the happy path flat and the binding in scope.Iterators
Lazy iterator chains — no intermediate allocations
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// JS: each step creates a new intermediate array
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n * n)
.reduce((sum, n) => sum + n, 0);
console.log(result); // 220 let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Rust: lazy — no intermediate allocation; one pass through the data
let result: i32 = numbers.iter()
.copied()
.filter(|&n| n % 2 == 0)
.map(|n| n * n)
.sum();
println!("{}", result); // 220 Rust's iterator adapters (
filter, map, take, etc.) are lazy — no computation happens until a consuming method like .sum(), .collect(), or a for loop forces evaluation. JavaScript's array methods are eager: each call processes the entire array and allocates a new intermediate array. Rust's lazy iterators process elements one at a time through the entire chain with no intermediate allocations, which is more cache-friendly and memory-efficient. The .copied() call converts &i32 references to i32 values early in the chain.collect() — consuming an iterator into a collection
const words = ["hello", "world", "rust"];
const upper = words.map(word => word.toUpperCase());
console.log(upper); // ["HELLO", "WORLD", "RUST"]
const lengths = words.map(word => word.length);
const total = lengths.reduce((sum, n) => sum + n, 0);
console.log(total); // 14 let words = vec!["hello", "world", "rust"];
let upper: Vec<String> = words.iter().map(|word| word.to_uppercase()).collect();
println!("{:?}", upper); // ["HELLO", "WORLD", "RUST"]
let lengths: Vec<usize> = words.iter().map(|word| word.len()).collect();
let total: usize = lengths.iter().sum();
println!("{}", total); // 14 .collect() consumes an iterator and assembles the results into a collection — the type annotation (Vec<String>) tells it what to build. Beyond Vec, .collect() can also produce HashMap, HashSet, and String. Specialized consumers like .sum(), .product(), .min(), and .max() are more idiomatic than fold(0, |acc, item| acc + item) when they apply. Calling collect() is always necessary because Rust iterators are lazy and do no work unless consumed.take, skip, enumerate, zip
const names = ["Alice", "Bob", "Carol", "Dave"];
const scores = [95, 87, 92, 78];
console.log(names.slice(0, 2)); // first 2
console.log(names.slice(2)); // after first 2
names.forEach((name, index) => console.log(`${index}: ${name}`));
const paired = names.map((name, i) => [name, scores[i]]);
console.log(paired); let names = vec!["Alice", "Bob", "Carol", "Dave"];
let scores = vec![95u32, 87, 92, 78];
let first_two: Vec<_> = names.iter().take(2).collect();
println!("{:?}", first_two);
let after_two: Vec<_> = names.iter().skip(2).collect();
println!("{:?}", after_two);
for (index, name) in names.iter().enumerate() {
println!("{}: {}", index, name);
}
let paired: Vec<_> = names.iter().zip(scores.iter()).collect();
for (name, score) in &paired {
println!("{}: {}", name, score);
} Rust's iterator adaptor library is extensive.
.take(n) yields the first n elements (like .slice(0, n)); .skip(n) skips the first n (like .slice(n)); .enumerate() adds an index; .zip(other) pairs elements from two iterators. Unlike JavaScript's equivalent array methods, these allocate no intermediate memory — they are lazy adapters that transform elements one at a time. Calling .take(2) on an infinite range (0..)) is perfectly safe and efficient in Rust.flat_map — flatten and transform in one step
const sentences = ["Hello world", "Rust is fast", "Learn it today"];
const words = sentences.flatMap(sentence => sentence.split(" "));
console.log(words);
const longWords = words.filter(word => word.length > 4);
console.log(longWords); let sentences = vec!["Hello world", "Rust is fast", "Learn it today"];
let words: Vec<&str> = sentences.iter()
.flat_map(|sentence| sentence.split(' '))
.collect();
println!("{:?}", words);
let long_words: Vec<&str> = words.iter()
.copied()
.filter(|word| word.len() > 4)
.collect();
println!("{:?}", long_words); .flat_map(fn) applies a function that returns an iterator to each element and flattens the results — equivalent to JavaScript's .flatMap(). In the example, splitting each sentence returns a Split iterator, and flat_map merges all those iterators into a single stream of words. The .copied() call in the second chain converts &&str (a reference to a reference) to &str, a common step needed when using .iter() on a Vec<&str>.find, any, all, position
const temperatures = [18, 22, 31, 28, 15, 35, 9];
console.log(temperatures.find(t => t > 30)); // 31
console.log(temperatures.some(t => t > 30)); // true
console.log(temperatures.every(t => t > 0)); // true
console.log(temperatures.findIndex(t => t > 30)); // 2 let temperatures = vec![18i32, 22, 31, 28, 15, 35, 9];
println!("{:?}", temperatures.iter().find(|&&t| t > 30)); // Some(31)
println!("{}", temperatures.iter().any(|&t| t > 30)); // true
println!("{}", temperatures.iter().all(|&t| t > 0)); // true
println!("{:?}", temperatures.iter().position(|&t| t > 30)); // Some(2) Rust's iterator methods
find, any, all, and position correspond directly to JavaScript's find, some, every, and findIndex. The Rust variants return Option: find returns Option<&T> (not undefined) and position returns Option<usize> (not -1). Both any and all short-circuit — any stops at the first true match, and all stops at the first false — identical to JavaScript's behavior.Modules & Crates
use — bringing items into scope
// Named imports from standard library equivalents:
import { join } from "node:path";
// Using standard math:
const result = Math.sqrt(16);
console.log(result);
console.log(join("foo", "bar", "baz")); use std::collections::HashMap;
use std::f64::consts::PI;
fn main() {
let circumference = 2.0 * PI * 5.0;
println!("{:.4}", circumference);
let mut counts: HashMap<&str, u32> = HashMap::new();
counts.insert("hello", 1);
println!("{:?}", counts);
} Rust's
use statement brings items from a module path into the current scope — equivalent to JavaScript's named import (import { PI } from "./math"). Without use, items must be referred to by their full path (std::collections::HashMap::new()). Rust's standard library (std) is automatically available without installation. Third-party libraries (crates) are added to Cargo.toml and downloaded by cargo build — cargo serves the same role as npm.mod and pub — modules and visibility
// Modules in JS use explicit export/import across files.
// Simulating with an IIFE to show the same visibility concept:
const math = (() => {
function internalHelper() { return 42; } // private — not exported
function add(a, b) { return a + b; }
const VERSION = "1.0";
return { add, VERSION }; // exported API
})();
console.log(math.add(3, 4));
console.log(math.VERSION); mod math {
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub const VERSION: &str = "1.0";
fn internal_helper() -> i32 { 42 } // private — not accessible outside mod
}
fn main() {
println!("{}", math::add(3, 4));
println!("{}", math::VERSION);
// math::internal_helper(); // compile error — not public
} In Rust, items are private to their module by default — adding
pub is required to make them accessible from outside. This is the same concept as JavaScript's export, just expressed differently (opt-in public vs. opt-in export). The mod keyword defines an inline module; in production code, a separate file whose name matches the module is more common (mod math; loads math.rs). The double-colon :: is the path separator, replacing JavaScript's slash-based import paths.Cargo — the build tool and package manager
// package.json — dependencies and scripts:
// {
// "dependencies": { "lodash": "^4.17.21" },
// "scripts": { "start": "node index.js", "test": "jest" }
// }
// npm install && node index.js
// Standard library usage — no install needed:
const { createHash } = require("node:crypto");
const hash = createHash("sha256").update("hello").digest("hex");
console.log(hash.slice(0, 16)); // Cargo.toml — dependencies:
// [dependencies]
// serde = { version = "1", features = ["derive"] }
// reqwest = "0.12"
//
// cargo build # download + compile deps
// cargo run # build and run
// cargo test # run test suite
// cargo add serde # add a dependency
// Using std — no Cargo.toml entry needed:
use std::collections::HashSet;
fn main() {
let mut unique_words: HashSet<&str> = HashSet::new();
for word in "hello world hello rust world".split_whitespace() {
unique_words.insert(word);
}
let mut sorted: Vec<&str> = unique_words.into_iter().collect();
sorted.sort();
println!("{:?}", sorted);
} Rust's build tool and package manager is
cargo — the equivalent of npm and a bundler combined. Dependencies are listed in Cargo.toml with semantic version requirements; cargo build downloads and compiles them into native machine code. The crates ecosystem (crates.io) is Rust's equivalent of npm. Unlike npm, Cargo compiles all dependencies — there is no runtime module loading. The Rust standard library is intentionally lean; many things JavaScript includes by default (HTTP, regex, async runtimes) are separate crates in Rust.Concurrency
OS threads — true parallelism
// JS is single-threaded; concurrency uses the event loop
// Worker threads exist but communicate by message-passing only
const { Worker, isMainThread } = require("node:worker_threads");
if (isMainThread) {
console.log("Main thread running");
// Workers run in separate V8 isolates — no shared objects
} use std::thread;
fn main() {
let worker = thread::spawn(|| {
for count in 1..=3 {
println!("worker: step {}", count);
}
});
for count in 1..=2 {
println!("main: step {}", count);
}
worker.join().unwrap(); // wait for the thread to finish
} Rust uses OS threads that truly run in parallel on multiple CPU cores. JavaScript is single-threaded — concurrency is achieved through the event loop and async/await, while parallelism requires separate Worker threads that share no objects and communicate only by message passing. Rust threads can share memory directly, but the borrow checker enforces that all shared data is protected by synchronization primitives. The
join() call waits for the thread to complete and returns a Result (the thread might have panicked). If the code compiles, it is guaranteed free of data races.Channels — thread communication with mpsc
// JS Worker threads: all data is serialized/copied via postMessage
const { Worker, parentPort } = require("node:worker_threads");
// In a worker: parentPort.postMessage({ result: 42 });
// In main: worker.on("message", msg => console.log(msg.result));
console.log("JS workers communicate via serialized messages only"); use std::sync::mpsc;
use std::thread;
fn main() {
let (sender, receiver) = mpsc::channel();
let producer = thread::spawn(move || {
for message in ["first", "second", "third"] {
sender.send(message).unwrap();
}
});
for received in receiver { // loop exits when all senders are dropped
println!("Received: {}", received);
}
producer.join().unwrap();
} Rust's
std::sync::mpsc (multiple-producer, single-consumer) channels work similarly to JavaScript's postMessage: one end sends values, the other receives them. Unlike JavaScript Worker threads, which always serialize data (converting to and from a structured-clone representation), Rust channels move ownership of values between threads — no copying for types that do not implement Copy. The receiver can be iterated with a for loop, which blocks until the channel closes (all senders are dropped).Arc<Mutex<T>> — shared state between threads
// JS: SharedArrayBuffer + Atomics for shared memory across workers
const sharedBuffer = new SharedArrayBuffer(4);
const counter = new Int32Array(sharedBuffer);
Atomics.add(counter, 0, 1);
Atomics.add(counter, 0, 1);
console.log(Atomics.load(counter, 0)); // 2 use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u32));
let mut workers = vec![];
for _ in 0..4 {
let counter_clone = Arc::clone(&counter);
let worker = thread::spawn(move || {
let mut value = counter_clone.lock().unwrap();
*value += 1;
});
workers.push(worker);
}
for worker in workers { worker.join().unwrap(); }
println!("Final count: {}", *counter.lock().unwrap()); // 4
} Arc<T> (Atomic Reference Counted) is a thread-safe reference-counted pointer — it frees the value when all references are dropped, just like a garbage collector, but using atomic operations instead of a runtime. Mutex<T> wraps a value and requires acquiring a lock before accessing it, preventing data races. The combination Arc<Mutex<T>> is the standard Rust pattern for shared mutable state between threads — analogous to JavaScript's SharedArrayBuffer + Atomics, but ergonomic and type-safe. Failing to lock (or dropping the lock early) is a compile error or a deliberate choice, not a silent race condition.async / await
async function double(value) {
return value * 2;
}
async function main() {
const first = await double(21);
console.log(first);
}
main(); use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};
// std has async/await syntax but ships no executor, so here is a tiny one.
// Real programs reach for tokio or async-std instead of hand-rolling this.
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = pin!(future);
let mut context = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(value) = future.as_mut().poll(&mut context) {
return value;
}
}
}
async fn double(value: i32) -> i32 {
value * 2
}
fn main() {
let result = block_on(async {
let first = double(21).await;
first
});
println!("{result}");
} Both languages use
async/await, but they differ in one telling way. A JavaScript async function is eager — calling it starts the work immediately and hands back a Promise the event loop will settle. A Rust async fn is lazy — calling it just builds a Future that does nothing until something polls it, which .await does. JavaScript has the event loop baked into every runtime, so main() just runs; Rust's std ships no executor, so production code adds a runtime such as tokio. The tiny block_on here stands in for that, polling one future to completion.