Skip to content

Rust for JavaScript developers: the leap worth taking

Tuan Nguyen Duc Anh
Published date:
Edit this post

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/TypeScriptRust equivalent
numberi32, u32, f64, …
stringString (heap) / &str (slice)
T | nullOption<T>
T | ErrorResult<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

  1. The Rust Book — the best documentation of any language.
  2. Rustlings — interactive exercises in the terminal.
  3. Rust by Example — learn with real examples.
  4. Build something with wasm-pack and 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.

Previous
TypeScript 5.x: features that change how you write code
Next
Urban photography: finding the frame in the chaos of the city