Skip to main content
Back to Journal
RustProgramming

Rust for JavaScript Developers: First Steps in Systems Programming

I spend most of my time writing JavaScript and TypeScript. But the tools I use every day (esbuild, SWC, parts of VS Code) are written in systems languages like Go and Rust. When I saw that SWC (a JavaScript/TypeScript compiler) written in Rust was 20 times faster than Babel, I got curious enough to learn the language.

I have spent the past two months working through the Rust Book and building small projects. Here is my honest report as someone coming from a JavaScript background.

Cargo: npm But Better

If there is one thing Rust absolutely nails, it is the package manager. Cargo is what npm should be. It handles project creation, dependency management, building, testing, documentation, and publishing. One tool, well designed.

cargo new my-project    # Create a new project
cargo build             # Compile
cargo run               # Compile and run
cargo test              # Run tests
cargo add serde         # Add a dependency

The Cargo.toml file is like package.json but cleaner. Dependencies are added with version ranges, and Cargo.lock pins exact versions (like package-lock.json). The ecosystem at crates.io has thousands of well-maintained packages.

Ownership: The Big Idea

This is the concept that makes Rust unique and is also the biggest source of frustration for newcomers. In JavaScript, you can pass objects around freely. Any function can hold a reference to any object, and the garbage collector cleans up when nothing references it anymore.

Rust has no garbage collector. Instead, it uses an ownership system. Every value has exactly one owner. When the owner goes out of scope, the value is dropped (freed). You can lend references to a value, but the compiler enforces strict rules about how many references can exist and whether they are mutable.

fn main() {
    let name = String::from("Bryan");
    greet(name);
    // println!("{}", name); // ERROR: name was moved to greet
}

fn greet(name: String) {
    println!("Hello, {}!", name);
} // name is dropped here

In JavaScript terms, imagine if passing an object to a function meant you could no longer use it in the calling code. That is what "move semantics" means. The value moved into greet, and the caller no longer owns it.

To keep access, you pass a reference (a borrow):

fn main() {
    let name = String::from("Bryan");
    greet(&name);
    println!("Still mine: {}", name); // works fine
}

fn greet(name: &String) {
    println!("Hello, {}!", name);
}

The & means "borrow this value, but I promise not to modify it, and the original owner still controls when it gets dropped." The compiler verifies these guarantees at compile time. This eliminates entire categories of bugs: use after free, data races, dangling pointers.

Structs and Enums vs JavaScript Objects

Where JavaScript has objects and classes, Rust has structs and enums. They are more rigid but also more expressive.

struct User {
    name: String,
    age: u32,
    active: bool,
}

impl User {
    fn new(name: String, age: u32) -> User {
        User { name, age, active: true }
    }

    fn display(&self) {
        println!("{} (age {})", self.name, self.age);
    }
}

Enums in Rust are much more powerful than JavaScript strings or constants. Each variant can carry data:

enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle(f64, f64, f64),
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
        Shape::Triangle(a, b, c) => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}

The match expression is like a switch statement that the compiler verifies is exhaustive. If you add a new variant to the enum, the compiler tells you every place in the code that needs to handle it. This is incredibly useful for preventing bugs when you extend your data model.

Error Handling with Result and Option

Rust does not have exceptions. Instead, functions that can fail return a Result type, and functions that might not have a value return an Option type.

use std::fs;

fn read_config(path: &str) -> Result<String, std::io::Error> {
    fs::read_to_string(path)
}

fn main() {
    match read_config("config.toml") {
        Ok(content) => println!("Config: {}", content),
        Err(e) => println!("Error reading config: {}", e),
    }
}

The ? operator is Rust's way of propagating errors without verbose match blocks:

fn process_config(path: &str) -> Result<Config, Box<dyn Error>> {
    let content = fs::read_to_string(path)?; // returns Err early if this fails
    let config: Config = toml::from_str(&content)?;
    Ok(config)
}

Coming from JavaScript where errors are either ignored or caught with try/catch, the explicit error handling in Rust felt tedious at first. But after a while, I realized it forced me to think about failure cases that I would have ignored in JavaScript.

Compiling to WebAssembly

One of the reasons Rust interested me as a JavaScript developer is WebAssembly. You can compile Rust functions to Wasm and call them from JavaScript in the browser or Node.js.

// lib.rs
#[no_mangle]
pub fn fibonacci(n: u32) -> u32 {
    if n <= 1 { return n; }
    fibonacci(n - 1) + fibonacci(n - 2)
}

Compile with wasm-pack build and you get a .wasm file you can import from JavaScript. For CPU-intensive tasks like image processing, parsing, or cryptography, running Rust via Wasm in the browser can be 10 to 50 times faster than the equivalent JavaScript.

The Learning Curve

I will not sugarcoat it: Rust is hard to learn. The borrow checker will reject code that looks perfectly fine to you, and the error messages, while helpful, can be overwhelming. I spent my first week fighting the compiler more than writing features.

But around the two-week mark, something clicked. I started thinking about ownership naturally, and my code compiled on the first try more often. The compiler stops being an adversary and starts being a collaborator. When it compiles, you have a high degree of confidence that it is correct.

Would I use Rust for a web API? Probably not, when Node.js or Go would be faster to build. Would I use it for a performance-critical library, a CLI tool, or a WebAssembly module? Absolutely. Learning Rust has also made me write better JavaScript, because I now think more carefully about ownership and lifetimes even in a garbage-collected language.

rustsystems-programmingownershipcargowasm