Rust appeared on web developers’ radars years ago, but adoption was slow. In 2026 the landscape changed: Rust powers critical tools in the JS ecosystem (Biome, Oxc, Rolldown, the SWC compiler) and WebAssembly makes it indispensable on the frontend. It’s time to learn it.
Table of contents
Open Table of contents
The biggest mindset shift: ownership
In JavaScript the garbage collector manages memory. In Rust the responsibility passes to the compiler through the ownership system.
// In JS: this works
// let a = [1, 2, 3];
// let b = a; // a is still valid
// In Rust:
fn main() {
let a = vec![1, 2, 3];
let b = a; // a is "moved" to b
println!("{:?}", a); // ✗ ERROR: a was moved
println!("{:?}", b); // ✓
}ownership.rs
The solution: borrowing with references.
fn main() {
let a = vec![1, 2, 3];
let b = &a; // immutable borrow
println!("{:?}", a); // ✓ a is still valid
println!("{:?}", b); // ✓
}
fn print_vec(v: &Vec<i32>) { // receives reference, not ownership
for n in v {
print!("{} ", n);
}
}borrowing.rs
Types: from any to the safest system in the world
| JavaScript/TypeScript | Rust equivalent |
|---|---|
number | i32, u32, f64, … |
string | String (heap) / &str (slice) |
T | null | Option<T> |
T | Error | Result<T, E> |
any[] | Vec<T> |
{ [key: string]: T } | HashMap<String, T> |
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None // equivalent to null without the billion-dollar mistake
} else {
Some(a / b)
}
}
fn main() {
match divide(10.0, 0.0) {
Some(result) => println!("Result: {result}"),
None => println!("Division by zero"),
}
}types.rs
Error handling: Result is the Promise of Rust
In JS you handle errors with try/catch or Promise chains. In Rust, Result<T, E> is the idiomatic way:
use std::fs;
use std::io;
// Before: without the ? operator
fn read_config_verbose() -> Result<String, io::Error> {
let content = match fs::read_to_string("config.toml") {
Ok(c) => c,
Err(e) => return Err(e),
};
Ok(content.to_uppercase())
}
// With the ? operator (equivalent to JS await, but for errors)
fn read_config() -> Result<String, io::Error> {
let content = fs::read_to_string("config.toml")?;
Ok(content.to_uppercase())
}errors.rs
Closures and higher-order functions
The syntax is different but the concept is identical:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// map + filter + collect (like Array.map + filter in JS)
let double_evens: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect();
println!("{:?}", double_evens); // [4, 8]
}closures.rs
Rust → WebAssembly: the bridge to the frontend
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}lib.rs
# Compile to WASM
wasm-pack build --target web
import init, { fibonacci } from "./pkg/my_project.js";
await init();
console.log(fibonacci(40)); // ~10x faster than pure JS versionmain.js
Where to start
- The Rust Book — the best documentation of any language.
- Rustlings — interactive exercises in the terminal.
- Rust by Example — learn with real examples.
- Build something with
wasm-packand use it from your current web project.
The learning curve is real, but the Rust compiler is the best teacher you’ll find: its error messages are detailed, accurate, and almost always include the solution.