You've heard it everywhere: "Rust is the future." Stack Overflow calls it the most admired language for the 8th year running. Discord rebuilt their infrastructure in it. Cloudflare runs critical edge services on it. Figma's multiplayer engine? Rust. Even Microsoft is rewriting core Windows components in Rust.
But here's the thing—if you're a JavaScript or TypeScript developer, every Rust tutorial feels like it was written for someone with a C++ background. Ownership? Borrowing? Lifetimes? The terminology alone is enough to make you close the tab and go back to your comfortable npm install.
This guide is different. We're going to learn Rust through the lens of a JavaScript developer. By the end, you'll have written your first 1000 lines of Rust and actually understand what's happening under the hood.
Why JavaScript Developers Should Care About Rust
Before we write any code, let's address the elephant in the room: Why would a web developer learn a systems programming language?
The Performance Reality Check
JavaScript is interpreted (or JIT-compiled). Rust compiles to native machine code. The difference isn't subtle:
// JavaScript: Parse JSON, find max value
const data = JSON.parse(hugeJsonString);
const max = Math.max(...data.numbers);
// Runtime: ~450ms for 10 million numbers
// Rust: Same operation
let data: Data = serde_json::from_str(&huge_json_string)?;
let max = data.numbers.iter().max();
// Runtime: ~12ms for 10 million numbers
That's not a typo—37x faster for the same logical operation. And this matters when you're:
- Building CLI tools that need to feel instant
- Processing large files (think build tools, linters)
- Writing serverless functions where cold start time = money
- Creating WebAssembly modules for compute-heavy browser tasks
The WebAssembly Bridge
Here's where it gets interesting for web developers. Rust compiles to WebAssembly (WASM) better than any other language:
// This Rust code...
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2)
}
}
...becomes a .wasm file you can import directly into JavaScript:
import init, { fibonacci } from './pkg/my_rust_lib.js';
await init();
console.log(fibonacci(40)); // Runs 10-20x faster than pure JS
Companies like Figma, Photoshop (web version), and Google Earth use this exact pattern for performance-critical code paths.
Your First Rust Program: Comparing to JavaScript
Let's start with something familiar. Here's a simple program in both languages:
JavaScript:
function greet(name) {
const message = `Hello, ${name}!`;
console.log(message);
}
greet("World");
Rust:
fn greet(name: &str) {
let message = format!("Hello, {}!", name);
println!("{}", message);
}
fn main() {
greet("World");
}
Already you can see some differences:
-
fninstead offunction - Types are explicit:
name: &str -
letworks similarly, butconstdoesn't exist the same way -
format!andprintln!have!because they're macros - Every Rust program needs a
mainfunction
Setting Up Your Environment
Before we go further, let's get Rust installed:
# Install Rust (works on macOS, Linux, WSL)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
rustc --version
cargo --version
cargo is Rust's package manager and build tool—think of it as npm + webpack combined.
# Create a new project (like npm init)
cargo new my-first-rust-app
cd my-first-rust-app
# Run the project
cargo run
The Big Three: Ownership, Borrowing, and Lifetimes
This is where JavaScript developers usually get lost. Let's break it down with JavaScript analogies.
Problem: JavaScript's Hidden Memory Management
In JavaScript, you never think about memory:
function processData() {
const data = [1, 2, 3, 4, 5]; // Memory allocated
const doubled = data.map(x => x * 2); // More memory allocated
return doubled;
} // Memory... eventually garbage collected
// You have no control over when memory is freed
// This can cause unexpected GC pauses in performance-critical code
JavaScript uses garbage collection. It's convenient but unpredictable. Rust gives you control without the footguns of manual memory management.
Ownership: One Owner, Always
In Rust, every value has exactly one owner:
fn main() {
let s1 = String::from("hello"); // s1 owns the string
let s2 = s1; // Ownership MOVES to s2
// println!("{}", s1); // ERROR! s1 no longer owns anything
println!("{}", s2); // Works fine
}
In JavaScript terms, imagine if this happened:
// Hypothetical "ownership" in JavaScript
let s1 = "hello";
let s2 = s1; // In Rust, this would invalidate s1
console.log(s1); // In Rust, this would be an error!
Why does Rust do this? Because when s2 goes out of scope, Rust knows exactly when to free the memory. No garbage collector needed.
Borrowing: References Without Ownership
But wait—what if you just want to use a value without taking ownership? That's borrowing:
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it doesn't own the String, so nothing happens
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // We BORROW s1 with &
println!("The length of '{}' is {}.", s1, len); // s1 still valid!
}
Think of & as saying "I'm just looking, not taking."
JavaScript comparison:
// JavaScript passes objects by reference anyway
function calculateLength(s) {
return s.length;
}
const s1 = "hello";
const len = calculateLength(s1);
console.log(`The length of '${s1}' is ${len}.`); // Works the same
The difference? In Rust, the compiler guarantees that calculate_length can't modify or keep s1. In JavaScript, you just have to trust the function.
Mutable Borrowing: The One-Writer Rule
In Rust, you can have either:
-
Many immutable references (
&T) - OR one mutable reference (
&mut T)
Never both at the same time.
fn main() {
let mut s = String::from("hello");
let r1 = &s; // Fine: immutable borrow
let r2 = &s; // Fine: another immutable borrow
// let r3 = &mut s; // ERROR! Can't mutably borrow while immutably borrowed
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // Now it's fine!
r3.push_str(", world");
}
Why this matters: This rule prevents data races at compile time. In JavaScript, you've probably encountered bugs where one part of your code mutates an object while another part is reading it. Rust makes this impossible.
Common Patterns: JavaScript vs Rust
Let's translate some patterns you use daily.
Arrays and Iteration
JavaScript:
const numbers = [1, 2, 3, 4, 5];
// Map
const doubled = numbers.map(x => x * 2);
// Filter
const evens = numbers.filter(x => x % 2 === 0);
// Reduce
const sum = numbers.reduce((acc, x) => acc + x, 0);
// Find
const firstEven = numbers.find(x => x % 2 === 0);
Rust:
let numbers = vec![1, 2, 3, 4, 5];
// Map
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
// Filter
let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
// Reduce (called fold in Rust)
let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x);
// Or simply:
let sum: i32 = numbers.iter().sum();
// Find
let first_even = numbers.iter().find(|x| *x % 2 == 0);
Key differences:
-
|x|is Rust's closure syntax (like arrow functions) -
.iter()creates an iterator -
.collect()turns an iterator back into a collection - You need to specify types or let Rust infer them
Optional Values (null handling)
JavaScript:
function findUser(id) {
const user = database.get(id);
if (user === null || user === undefined) {
return "Anonymous";
}
return user.name;
}
// Or with optional chaining
const name = user?.name ?? "Anonymous";
Rust:
fn find_user(id: u32) -> String {
let user = database.get(id);
match user {
Some(u) => u.name.clone(),
None => String::from("Anonymous"),
}
}
// Or more concisely
let name = user.map(|u| u.name.clone()).unwrap_or(String::from("Anonymous"));
// Or even simpler with if let
if let Some(user) = database.get(id) {
println!("Found: {}", user.name);
}
Rust has no null. Instead, you use Option<T>:
-
Some(value)= there's a value -
None= there's no value
The compiler forces you to handle both cases. No more "undefined is not an object" errors!
Error Handling
JavaScript:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Fetch failed:", error);
throw error;
}
}
Rust:
use reqwest;
async fn fetch_data(url: &str) -> Result<serde_json::Value, reqwest::Error> {
let response = reqwest::get(url).await?;
let data = response.json().await?;
Ok(data)
}
// Using it
match fetch_data("https://api.example.com/data").await {
Ok(data) => println!("Got data: {:?}", data),
Err(e) => eprintln!("Fetch failed: {}", e),
}
The ? operator is Rust's equivalent of "if this fails, return the error immediately." It's like automatic try-catch propagation.
Building Something Real: A JSON CLI Tool
Let's build a CLI tool that JavaScript developers would use—a JSON formatter:
use std::env;
use std::fs;
use serde_json::{Value, to_string_pretty};
fn main() {
// Get command line arguments (like process.argv)
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <file.json>", args[0]);
std::process::exit(1);
}
let filename = &args[1];
// Read file (like fs.readFileSync)
let contents = match fs::read_to_string(filename) {
Ok(c) => c,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
// Parse JSON
let parsed: Value = match serde_json::from_str(&contents) {
Ok(v) => v,
Err(e) => {
eprintln!("Invalid JSON: {}", e);
std::process::exit(1);
}
};
// Pretty print
match to_string_pretty(&parsed) {
Ok(pretty) => println!("{}", pretty),
Err(e) => eprintln!("Error formatting: {}", e),
};
}
To run this:
# Add dependency to Cargo.toml
# [dependencies]
# serde_json = "1.0"
cargo build --release
./target/release/json-formatter messy.json
The compiled binary is ~1MB and runs in milliseconds—compare that to shipping Node.js with your CLI tool.
Async Rust: It's Not That Different
Modern JavaScript is all about async/await. Rust has it too:
JavaScript:
async function fetchMultiple(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.all(promises);
return results;
}
Rust (with tokio runtime):
use futures::future::join_all;
async fn fetch_multiple(urls: Vec<&str>) -> Vec<Result<String, reqwest::Error>> {
let futures = urls.iter().map(|url| async move {
let response = reqwest::get(*url).await?;
response.text().await
});
join_all(futures).await
}
#[tokio::main]
async fn main() {
let urls = vec![
"https://api.example.com/1",
"https://api.example.com/2",
];
let results = fetch_multiple(urls).await;
for result in results {
match result {
Ok(body) => println!("Got: {:.100}...", body),
Err(e) => eprintln!("Error: {}", e),
}
}
}
The structure is remarkably similar! The main difference is that Rust needs an explicit async runtime (tokio is the most popular).
The Rust Ecosystem for Web Developers
Here are the crates (Rust's npm packages) you'll use most:
Web Frameworks
- Axum - The new standard, built by the Tokio team
- Actix Web - Battle-tested, extremely fast
- Rocket - Developer-friendly, great ergonomics
Serialization
- Serde - The de-facto standard for JSON, YAML, TOML, etc.
- serde_json - JSON specifically
HTTP Client
- Reqwest - Like axios for Rust
CLI Tools
- Clap - Argument parsing (like commander.js)
- Indicatif - Progress bars
- Colored - Terminal colors
WebAssembly
- wasm-bindgen - JS/Rust interop
- wasm-pack - Build and publish WASM packages
Performance Comparison: Real Numbers
Let's compare a realistic workload—processing a 100MB JSON file:
| Task | Node.js | Rust | Speedup |
|---|---|---|---|
| Parse JSON | 2.3s | 0.18s | 12.7x |
| Find all emails (regex) | 4.1s | 0.31s | 13.2x |
| Transform & serialize | 3.8s | 0.24s | 15.8x |
| Memory usage | 890MB | 210MB | 4.2x less |
These numbers matter when you're:
- Building build tools (like esbuild, written in Go for similar reasons)
- Processing logs or large datasets
- Running in memory-constrained environments (serverless, edge)
Common Gotchas for JavaScript Developers
1. Strings Are Complicated
let s1 = "hello"; // &str - a string slice (borrowed)
let s2 = String::from("hello"); // String - an owned string
// You can't do this:
// let s3: String = "hello"; // Error!
// You need to convert:
let s3: String = "hello".to_string();
let s4: String = String::from("hello");
Rule of thumb: Use &str for function parameters, String when you need to own the data.
2. No Exceptions, Only Results
// This won't compile - you must handle the Result
let file = File::open("data.txt"); // Returns Result<File, Error>
// You must handle it
let file = File::open("data.txt")?; // Propagate error
// or
let file = File::open("data.txt").unwrap(); // Panic if error
// or
let file = File::open("data.txt").expect("Failed to open file"); // Panic with message
3. Immutability Is Default
let x = 5;
// x = 6; // Error! Variables are immutable by default
let mut y = 5;
y = 6; // Works!
This is the opposite of JavaScript's let (mutable) vs const (immutable).
4. No Implicit Type Coercion
let x: i32 = 5;
let y: i64 = 10;
// let z = x + y; // Error! Can't add i32 and i64
let z = x as i64 + y; // Must explicitly convert
Your Learning Path Forward
Here's a realistic roadmap for JavaScript developers:
Week 1-2: Basics
- Complete the first 8 chapters of "The Rust Book" (free online)
- Write small programs: FizzBuzz, file reader, simple CLI
Week 3-4: Ownership Deep Dive
- Re-read the ownership chapters
- Complete Rustlings exercises (interactive practice)
- Build a TODO CLI app with file persistence
Month 2: Web Development
- Build a REST API with Axum
- Connect to a database (SQLx or Diesel)
- Deploy to a cloud platform
Month 3: WebAssembly
- Build a WASM module
- Integrate it with a React/Vue/Svelte app
- Compare performance with pure JavaScript
Conclusion: Is Rust Worth It?
For JavaScript developers, Rust isn't a replacement—it's a complement. You'll still write your web apps in TypeScript. But when you need:
- Maximum performance for compute-heavy tasks
- Predictable latency without GC pauses
- Small binaries for CLI tools or serverless
- WebAssembly for browser performance
...Rust is the answer.
The learning curve is real. Ownership and borrowing will confuse you at first. The compiler will reject your code constantly (but its error messages are genuinely helpful).
But once it clicks—and it will—you'll have a superpower that most JavaScript developers don't. You'll understand how memory actually works. You'll write safer code in any language. And you'll have a tool that can solve problems JavaScript simply can't.
Start with something small. A JSON formatter. A file renamer. A simple CLI tool. Let the compiler teach you. And before you know it, you'll be writing Rust that runs 20x faster than your JavaScript ever could.
Welcome to Rust. The compiler is strict, but it's on your side.
⚡ Speed Tip: Read the original post on the Pockit Blog.
Tired of slow cloud tools? Pockit.tools runs entirely in your browser with zero latency. Try the fastest JSON Formatter & Diff Checker now.
Top comments (0)