Learn Rust by Javascript

JavaScript vs. Rust: A Comprehensive Comparison for JavaScript Developers

This document aims to provide a comprehensive comparison between JavaScript and Rust, tailored for JavaScript developers looking to understand Rust’s paradigms and syntax. We will start with fundamental concepts and progressively move to more advanced topics, illustrating differences and similarities with practical code examples.


1. Introduction

JavaScript is a high-level, interpreted, dynamically typed language primarily known for web development. It’s multi-paradigm, supporting object-oriented, functional, and imperative programming styles. Its flexibility and vast ecosystem have made it incredibly popular.

Rust is a systems programming language focused on safety, performance, and concurrency. It’s statically typed and compiled, offering strong memory safety guarantees without a garbage collector. Rust is gaining traction in areas like web assembly, command-line tools, and backend services due to its performance and reliability.

The key difference for a JavaScript developer transitioning to Rust will be the shift from dynamic typing and garbage collection to static typing, ownership, and explicit memory management.


2. Variables and Data Types

Declaration and Initialization

JavaScript: Variables are declared using var, let, or const. let and const are block-scoped, with const indicating a constant value that cannot be reassigned.

// Variable declaration and initialization
var oldSchool = "Hello, var!";
let mutableVar = 10;
const constantVar = "I cannot be changed.";

console.log(oldSchool); // Hello, var!
console.log(mutableVar); // 10
console.log(constantVar); // I cannot be changed.

mutableVar = 20; // Reassignment is allowed for `let`
// constantVar = "Trying to change"; // This would throw an error

Rust: Variables are declared using let. By default, variables in Rust are immutable. To make a variable mutable, you must explicitly use the mut keyword.

// Variable declaration and initialization (immutable by default)
let immutable_var = "Hello, Rust!";
let mut mutable_var = 10; // Use 'mut' for mutable variables
const CONSTANT_VAR: &str = "I am a constant."; // Constants require explicit type annotation

println!("{}", immutable_var); // Hello, Rust!
println!("{}", mutable_var); // 10
println!("{}", CONSTANT_VAR); // I am a constant.

mutable_var = 20; // Reassignment is allowed for 'mut' variables
println!("{}", mutable_var); // 20
// immutable_var = "Trying to change"; // This would be a compile-time error

Type Inference and Annotations

JavaScript: JavaScript is dynamically typed, meaning you don’t declare the type of a variable. The type is determined at runtime.

let dynamicVar = 10; // Type is number
dynamicVar = "Now I am a string"; // Type changes to string

console.log(typeof dynamicVar); // string

Rust: Rust is statically typed, but it has strong type inference. You often don’t need to explicitly write down the type, as the compiler can usually figure it out. However, you can add type annotations for clarity or when inference isn’t possible (e.g., in const declarations).

// Type inference
let inferred_num = 50; // Rust infers this as an i32 (default integer type)
let inferred_text = "Rust is cool"; // Rust infers this as &str (string slice)

// Explicit type annotation
let explicit_float: f64 = 3.14; // 'f64' for 64-bit floating point
let explicit_bool: bool = true;

println!("Inferred number: {}", inferred_num);
println!("Inferred text: {}", inferred_text);
println!("Explicit float: {}", explicit_float);
println!("Explicit boolean: {}", explicit_bool);

Mutability

This was covered in “Declaration and Initialization” but is important enough to re-emphasize.

JavaScript: Variables declared with let can be reassigned. Variables declared with const cannot be reassigned, but if they hold an object or array, the contents of that object/array can still be modified.

let count = 0;
count = 1; // OK

const user = { name: "Alice" };
user.name = "Bob"; // OK, content of the object changed
// user = { name: "Charlie" }; // Error: Assignment to constant variable.

Rust: Variables are immutable by default. To allow reassignment, you must use the mut keyword. This encourages a functional programming style where data is immutable unless explicitly stated.

let x = 5;
// x = 6; // Error: cannot assign twice to immutable variable `x`

let mut y = 5;
y = 6; // OK

println!("x: {}, y: {}", x, y);

Basic Data Types

JavaScript:

  • Primitives: Number, BigInt, String, Boolean, Symbol, null, undefined.
  • Objects: Object, Array, Function, Date, RegExp, etc.
let num = 123;
let bigNum = 12345678901234567890n; // BigInt
let str = "Hello";
let bool = true;
let sym = Symbol('id');
let empty = null;
let notDefined; // undefined

console.log(typeof num); // number
console.log(typeof bigNum); // bigint
console.log(typeof str); // string
console.log(typeof bool); // boolean
console.log(typeof sym); // symbol
console.log(typeof empty); // object (historical quirk)
console.log(typeof notDefined); // undefined

Rust: Rust has a richer set of primitive types, focusing on explicit sizes for integers and floating-point numbers.

  • Integers: i8, i16, i32, i64, i128 (signed) and u8, u16, u32, u64, u128 (unsigned). isize and usize depend on the architecture (32-bit or 64-bit).
  • Floating-Point Numbers: f32, f64.
  • Booleans: bool (true or false).
  • Characters: char (single Unicode scalar value).
  • Strings: String (growable, owned, UTF-8 encoded) and &str (string slice, borrowed reference). We’ll dive deeper into these later.
  • Tuples: Fixed-size collection of values of different types.
  • Arrays: Fixed-size collection of values of the same type.
let i32_val: i32 = -100;
let u8_val: u8 = 255;
let float_val: f64 = 99.9;
let boolean_val: bool = false;
let character_val: char = '😊'; // Unicode character

println!("i32: {}", i32_val);
println!("u8: {}", u8_val);
println!("f64: {}", float_val);
println!("bool: {}", boolean_val);
println!("char: {}", character_val);

Type Conversion (Casting)

JavaScript: JavaScript has implicit type coercion (e.g., 1 + "2" results in "12") and explicit conversion functions like Number(), String(), Boolean(), parseInt(), parseFloat().

let numStr = "123";
let num = Number(numStr); // Explicit conversion to number
let parsedInt = parseInt("456px"); // Parses "456"

let numToBecomeStr = 789;
let strFromNum = String(numToBecomeStr); // Explicit conversion to string

console.log(num + 1); // 124
console.log(parsedInt); // 456
console.log(strFromNum + " is a string"); // "789 is a string"

Rust: Rust does not have implicit type coercion between primitive types (except for some numeric literals). You must use explicit casting with the as keyword. This prevents subtle bugs that can arise from unexpected type conversions.

let int_val: i32 = 10;
let float_val: f64 = int_val as f64; // Convert i32 to f64

println!("Int as float: {}", float_val); // 10.0

let large_num: i32 = 257;
let small_num: u8 = large_num as u8; // Be careful with casting larger types to smaller ones (truncation)

println!("Large num as u8: {}", small_num); // 1 (257 % 256 = 1)

let char_code: u8 = 65;
let char_val: char = char_code as char; // Convert u8 to char

println!("Char value: {}", char_val); // A

// String to number conversion usually requires parsing methods
let num_str = "123".to_string();
let parsed_int: i32 = num_str.parse().expect("Failed to parse string to int");
println!("Parsed int: {}", parsed_int); // 123

Printing Variables

JavaScript: The primary way to print to the console is console.log(). You can concatenate strings or use template literals.

let name = "Alice";
let age = 30;

console.log("Hello, " + name + "! You are " + age + " years old.");
console.log(`Hello, ${name}! You are ${age} years old.`); // Template literal

Rust: Rust uses macros for printing, primarily println!() for printing to the console with a newline, and print!() for printing without a newline. It uses a placeholder system similar to C-style printf.

let name = "Bob";
let age = 25;

// Basic placeholder {} for values that implement the Display trait
println!("Hello, {}! You are {} years old.", name, age);

// Positional arguments (less common, but possible)
println!("{0} is {1} years old. {0} loves Rust!", name, age);

// Named arguments (very readable)
println!("{person_name} is {person_age} years old.", person_name = name, person_age = age);

// Debug printing with {:?} (requires Debug trait implementation)
// This is useful for complex data structures
let tuple = (10, "hello");
println!("Debug tuple: {:?}", tuple);

3. Functions

Declaration

JavaScript: Functions can be declared using the function keyword, as function expressions, or as arrow functions.

// Function declaration
function greet(name) {
    return `Hello, ${name}!`;
}

// Function expression
const farewell = function(name) {
    return `Goodbye, ${name}.`;
};

// Arrow function
const add = (a, b) => a + b;

console.log(greet("Alice")); // Hello, Alice!
console.log(farewell("Bob")); // Goodbye, Bob.
console.log(add(5, 3)); // 8

Rust: Functions are declared using the fn keyword. Function arguments and return types must be explicitly annotated. Rust functions use snake_case for naming.

// Function declaration
fn greet(name: &str) -> String {
    format!("Hello, {}!", name) // 'format!' macro creates a String
}

// Another function, no return value (implicitly returns ())
fn say_goodbye(name: &str) {
    println!("Goodbye, {}.", name);
}

// Function with multiple parameters and a return value
fn add(a: i32, b: i32) -> i32 {
    a + b // No semicolon means this is the return value (expression-based return)
}

fn main() {
    println!("{}", greet("Alice")); // Hello, Alice!
    say_goodbye("Bob"); // Goodbye, Bob.
    println!("5 + 3 = {}", add(5, 3)); // 5 + 3 = 8
}

Note on Rust’s return values: Rust is an expression-based language. The last expression in a function (without a semicolon) is implicitly returned. You can also use the return keyword explicitly.

fn multiply(a: i32, b: i32) -> i32 {
    if a == 0 || b == 0 {
        return 0; // Explicit return
    }
    a * b // Implicit return
}

Parameters and Return Values

JavaScript: Parameters can be of any type due to dynamic typing. Functions can return any type or undefined implicitly.

function processData(data, handler) {
    if (typeof data === 'number') {
        return handler(data * 2);
    } else if (typeof data === 'string') {
        return handler(data.toUpperCase());
    }
    return undefined;
}

console.log(processData(10, x => x + 5)); // 25
console.log(processData("hello", s => s + "!")); // HELLO!

Rust: Parameters and return values require explicit type annotations. This ensures type safety at compile time.

fn process_number(num: i32) -> i32 {
    num * 2
}

fn process_string(text: &str) -> String {
    text.to_uppercase() // &str to String conversion
}

fn main() {
    let result_num = process_number(10);
    println!("Processed number: {}", result_num); // Processed number: 20

    let result_str = process_string("hello");
    println!("Processed string: {}", result_str); // Processed string: HELLO
}

Arrow Functions vs. Closures

JavaScript: Arrow functions provide a concise syntax for function expressions and lexically bind this. They are widely used for callbacks and short functions.

const numbers = [1, 2, 3];

// Arrow function as a callback
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6]

// Arrow function with lexical 'this'
const myObject = {
    value: 10,
    getValue: function() {
        setTimeout(() => {
            console.log(this.value); // 'this' refers to myObject
        }, 100);
    }
};
myObject.getValue(); // 10 (after 100ms)

Rust: Rust has closures, which are anonymous functions that can capture values from their enclosing scope. They are analogous to JavaScript’s arrow functions in many use cases, especially for iterators and callbacks.

let numbers = vec![1, 2, 3]; // Vector, similar to JS array

// Closure used with `map` on an iterator
let doubled: Vec<i32> = numbers.iter().map(|num| num * 2).collect();
println!("{:?}", doubled); // [2, 4, 6]

let factor = 10;
// Closure capturing `factor` from its environment
let multiplier = |x: i32| x * factor;
println!("5 * factor = {}", multiplier(5)); // 50

// Closures can also capture by mutable reference or by value
let mut counter = 0;
let increment = || {
    counter += 1; // Mutably borrows 'counter'
    println!("Counter: {}", counter);
};
increment(); // Counter: 1
increment(); // Counter: 2

4. Control Flow

Conditional Statements (if/else)

JavaScript: Standard if/else if/else syntax. Conditions must resolve to a truthy or falsy value.

let score = 75;

if (score >= 90) {
    console.log("Grade A");
} else if (score >= 80) {
    console.log("Grade B");
} else {
    console.log("Grade C");
}

Rust: Similar if/else if/else syntax. Conditions must resolve to a bool. if is an expression, meaning it can return a value.

let score = 75;

if score >= 90 {
    println!("Grade A");
} else if score >= 80 {
    println!("Grade B");
} else {
    println!("Grade C");
}

// `if` as an expression
let grade = if score >= 90 {
    'A' // Character literal
} else if score >= 80 {
    'B'
} else {
    'C'
};
println!("Your grade is: {}", grade); // Your grade is: C

Note: When using if as an expression, all branches must return the same type.

Loops (for, while, loop)

JavaScript: Common loops include for (traditional and for...of/for...in), and while.

// Traditional for loop
for (let i = 0; i < 3; i++) {
    console.log(`JS For loop: ${i}`);
}

// for...of (iterating over iterable objects like arrays)
const arr = ["apple", "banana", "cherry"];
for (const item of arr) {
    console.log(`JS For...of: ${item}`);
}

// for...in (iterating over object properties - generally not recommended for arrays)
const obj = { a: 1, b: 2 };
for (const key in obj) {
    console.log(`JS For...in: ${key}: ${obj[key]}`);
}

// While loop
let count = 0;
while (count < 3) {
    console.log(`JS While loop: ${count}`);
    count++;
}

Rust: Rust has loop (infinite loop), while, and for (for iterating over ranges or collections).

// loop (infinite loop, typically used with `break`)
let mut i = 0;
loop {
    println!("Rust Loop: {}", i);
    i += 1;
    if i == 3 {
        break; // Exit the loop
    }
}

// while loop
let mut count = 0;
while count < 3 {
    println!("Rust While loop: {}", count);
    count += 1;
}

// for loop (iterating over a range)
for num in 0..3 { // 0 to 2 (exclusive of 3)
    println!("Rust For loop (range): {}", num);
}

// for loop (iterating over elements in a collection)
let fruits = vec!["apple", "banana", "cherry"];
for fruit in fruits { // 'fruit' takes ownership of elements if not using '&'
    println!("Rust For loop (vec): {}", fruit);
}

// Iterating with index
for (index, fruit) in fruits.iter().enumerate() {
    println!("Fruit at index {}: {}", index, fruit);
}

Match Expressions (Switch Equivalent)

JavaScript: The switch statement is used for multiple conditional branches based on a single value.

let day = "Monday";

switch (day) {
    case "Monday":
        console.log("Start of the week.");
        break; // Important to prevent fall-through
    case "Friday":
        console.log("Almost weekend!");
        break;
    default:
        console.log("Mid-week or weekend.");
}

Rust: Rust’s match expression is a powerful control flow construct similar to switch but much more versatile and safe, ensuring all possible cases are handled. It’s an expression, so it can return a value.

let day = "Monday";

let message = match day {
    "Monday" => "Start of the week.",
    "Friday" => "Almost weekend!",
    _ => "Mid-week or weekend.", // The underscore `_` is a wildcard, catching all other cases
};
println!("{}", message); // Start of the week.

// Match can be used with enums (discussed later) or complex patterns
let num = 7;
let description = match num {
    1 => "One",
    2 | 3 => "Two or Three", // Multiple patterns
    4..=6 => "Four, Five, or Six", // Range pattern (inclusive)
    _ if num % 2 == 0 => "An even number greater than 6", // Guard condition
    _ => "An odd number greater than 6",
};
println!("Number description: {}", description); // Number description: An odd number greater than 6

5. Data Structures

Arrays

JavaScript: Arrays are dynamic, mutable, and can hold elements of different types. They are fundamentally objects.

let jsArray = [1, "hello", true]; // Mixed types
console.log(jsArray[0]); // 1
console.log(jsArray.length); // 3

jsArray.push("world"); // Add element
jsArray[0] = 5; // Modify element
console.log(jsArray); // [5, "hello", true, "world"]

Rust: Arrays are fixed-size collections of elements of the same type. Their size is known at compile time.

// Fixed-size array, elements must be of the same type
let rust_array: [i32; 3] = [1, 2, 3];
println!("First element: {}", rust_array[0]); // 1
println!("Array length: {}", rust_array.len()); // 3

// Trying to modify an immutable array (error)
// rust_array[0] = 5; // Error: cannot assign to `rust_array[_]`

// To modify, the array must be mutable
let mut mutable_rust_array = [1, 2, 3];
mutable_rust_array[0] = 5;
println!("Modified array: {:?}", mutable_rust_array); // [5, 2, 3]

// Creating an array with repeated elements
let five_zeros = [0; 5]; // [0, 0, 0, 0, 0]
println!("Five zeros: {:?}", five_zeros);

Due to their fixed size, Rust arrays are less commonly used than Vec (vectors) for dynamic collections.

Vectors (Dynamic Arrays)

Rust:Vec<T> (vector) is Rust’s growable, heap-allocated list type. It’s the most common dynamic collection, similar to JavaScript arrays in behavior.

// Create an empty mutable vector
let mut numbers: Vec<i32> = Vec::new();
numbers.push(10);
numbers.push(20);
numbers.push(30);
println!("Vector: {:?}", numbers); // [10, 20, 30]

// Create a vector with initial values using the `vec!` macro
let mut fruits = vec!["apple", "banana"];
fruits.push("cherry");
println!("Fruits: {:?}", fruits); // ["apple", "banana", "cherry"]

// Accessing elements
println!("First fruit: {}", fruits[0]); // apple

// Getting an element safely using `get` (returns Option)
match fruits.get(1) {
    Some(fruit) => println!("Second fruit: {}", fruit),
    None => println!("No second fruit found."),
}

// Pop an element
let last_fruit = fruits.pop();
println!("Popped fruit: {:?}", last_fruit); // Some("cherry")
println!("Fruits after pop: {:?}", fruits); // ["apple", "banana"]

Strings

JavaScript: Strings are immutable sequences of characters. They are UTF-16 encoded.

let jsStr = "Hello World";
console.log(jsStr.length); // 11
console.log(jsStr[0]); // H (access by index is common)
console.log(jsStr.slice(0, 5)); // Hello
console.log(jsStr.toUpperCase()); // HELLO WORLD

Rust: Rust has two main string types:

  1. String: A growable, heap-allocated, owned, UTF-8 encoded string. It’s mutable.
  2. &str: A string slice, which is a borrowed reference to a string. It’s immutable and often used for string literals or views into Strings.
// &str (string slice - immutable, typically for literals)
let s_literal: &str = "Hello Rust";
println!("String literal: {}", s_literal);

// String (owned, growable, mutable)
let mut s_owned: String = String::from("Initial text");
s_owned.push_str(", more text"); // Append a string slice
s_owned.push('!'); // Append a character
println!("Owned string: {}", s_owned); // Initial text, more text!

// Converting between String and &str
let s_from_str: String = "Another string".to_string(); // &str to String
let s_as_slice: &str = &s_owned; // String to &str (borrowing)
println!("As slice: {}", s_as_slice);

// String length (number of bytes, not characters for unicode)
println!("Length of owned string (bytes): {}", s_owned.len());

// For character count, iterate over chars
println!("Character count: {}", "你好".chars().count()); // 2

Working with strings in Rust can be more nuanced due to UTF-8 encoding and the distinction between String and &str.

Objects vs. Structs

JavaScript: Objects are fundamental for grouping related data and behavior. They are essentially key-value maps.

// Object literal
let userJS = {
    name: "Alice",
    age: 30,
    isActive: true,
    address: {
        street: "123 Main St",
        city: "Anytown"
    },
    greet: function() { // Method
        console.log(`Hello, my name is ${this.name}.`);
    }
};

console.log(userJS.name); // Alice
userJS.age = 31; // Modify property
userJS.email = "alice@example.com"; // Add new property
userJS.greet(); // Hello, my name is Alice.

Rust:Structs are custom data types that let you name and package together multiple related values of potentially different types. Unlike JavaScript objects, struct fields are fixed and explicitly typed at compile time.

// Define a struct
struct User {
    name: String,
    age: u32,
    is_active: bool,
    address: Address, // Structs can contain other structs
}

// Define another struct for Address
struct Address {
    street: String,
    city: String,
}

// Implement methods for the User struct (similar to prototypes in JS)
impl User {
    // Constructor-like function (conventionally called `new`)
    fn new(name: String, age: u32, street: String, city: String) -> User {
        User {
            name, // Shorthand for name: name
            age,
            is_active: true,
            address: Address { street, city },
        }
    }

    // Method with a shared reference `&self`
    fn greet(&self) {
        println!("Hello, my name is {}.", self.name);
    }

    // Method with a mutable reference `&mut self`
    fn celebrate_birthday(&mut self) {
        self.age += 1;
        println!("Happy birthday, {}! You are now {}.", self.name, self.age);
    }
}

fn main() {
    // Create an instance of the struct
    let mut user1 = User {
        name: String::from("Bob"),
        age: 25,
        is_active: true,
        address: Address {
            street: String::from("456 Elm St"),
            city: String::from("Someville"),
        },
    };

    println!("User name: {}", user1.name); // Access field
    user1.age = 26; // Modify field (if `user1` is mutable)
    println!("User age: {}", user1.age);

    user1.greet(); // Call a method: Hello, my name is Bob.
    user1.celebrate_birthday(); // Happy birthday, Bob! You are now 27.

    // Using the constructor-like `new` function
    let mut user2 = User::new(
        String::from("Charlie"),
        40,
        String::from("789 Oak Ave"),
        String::from("Othertown"),
    );
    user2.greet(); // Hello, my name is Charlie.
}

Tuple Structs: Structs without named fields.

struct Color(i32, i32, i32); // R, G, B
let red = Color(255, 0, 0);
println!("Red: ({}, {}, {})", red.0, red.1, red.2);

Unit-like Structs: Structs with no fields, useful for traits.

struct AlwaysActive;

Enums

JavaScript: JavaScript doesn’t have a built-in enum type. Developers typically use objects with const properties or literal strings/numbers to achieve similar functionality.

// Using an object as an "enum"
const TrafficLight = {
    RED: "red",
    YELLOW: "yellow",
    GREEN: "green"
};

let currentLight = TrafficLight.RED;

if (currentLight === TrafficLight.RED) {
    console.log("Stop!");
}

Rust: Rust’s enum (enumerations) are powerful types that allow you to define a type by enumerating its possible variants. Each variant can optionally have associated data. This is significantly more robust than JavaScript’s object-based approach.

// Simple Enum
enum TrafficLight {
    Red,
    Yellow,
    Green,
}

// Enum with associated data (tuple variants)
enum Message {
    Quit,
    Move { x: i32, y: i32 }, // Anonymous struct variant
    Write(String),           // Tuple variant
    ChangeColor(i32, i32, i32),
}

impl TrafficLight {
    fn status(&self) -> &str {
        match self {
            TrafficLight::Red => "Stop!",
            TrafficLight::Yellow => "Slow down.",
            TrafficLight::Green => "Go!",
        }
    }
}

fn main() {
    let current_light = TrafficLight::Red;
    println!("Traffic light status: {}", current_light.status()); // Stop!

    let msg1 = Message::Quit;
    let msg2 = Message::Move { x: 10, y: 20 };
    let msg3 = Message::Write(String::from("hello"));
    let msg4 = Message::ChangeColor(0, 160, 255);

    // Using `match` with enums for pattern matching
    process_message(msg1);
    process_message(msg2);
    process_message(msg3);
    process_message(msg4);
}

fn process_message(msg: Message) {
    match msg {
        Message::Quit => {
            println!("The application will quit.");
        }
        Message::Move { x, y } => { // Destructure the struct variant
            println!("Move to x: {}, y: {}", x, y);
        }
        Message::Write(text) => { // Destructure the tuple variant
            println!("Writing message: {}", text);
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change color to R:{}, G:{}, B:{}", r, g, b);
        }
    }
}

Tuples

JavaScript: There isn’t a direct equivalent to Rust’s tuples. You would typically use a fixed-size array or an object if you need to group related, heterogeneous data.

// Using an array to simulate a tuple
let personInfo = ["Alice", 30, true];
console.log(personInfo[0]); // Alice

Rust: Tuples are a way to group together a fixed number of values of different types into one compound type.

// A tuple containing an integer, a float, and a string slice
let person_data: (i32, f64, &str) = (20, 3.14, "tuple_example");

// Accessing elements by index
println!("The integer is: {}", person_data.0);
println!("The float is: {}", person_data.1);
println!("The string is: {}", person_data.2);

// Destructuring a tuple (pattern matching)
let (age, pi, name) = person_data;
println!("Age: {}, Pi: {}, Name: {}", age, pi, name);

// Tuples can be used as function return values
fn get_user_details() -> (String, u32) {
    ("Bob".to_string(), 25)
}

let (user_name, user_age) = get_user_details();
println!("User: {}, Age: {}", user_name, user_age);

6. Error Handling

JavaScript: Error handling is typically done using try...catch blocks and throwing Error objects. Asynchronous errors are handled with catch on Promises or try...catch with async/await.

function divideJS(a, b) {
    if (b === 0) {
        throw new Error("Cannot divide by zero.");
    }
    return a / b;
}

try {
    let result = divideJS(10, 2);
    console.log("Result:", result); // Result: 5

    let errorResult = divideJS(10, 0);
    console.log("This won't be reached:", errorResult);
} catch (error) {
    console.error("Caught an error:", error.message); // Caught an error: Cannot divide by zero.
}

// Asynchronous error handling
async function fetchData() {
    try {
        // Simulate a failed fetch
        const response = await Promise.reject(new Error("Network error"));
        console.log(response);
    } catch (error) {
        console.error("Async error:", error.message); // Async error: Network error
    }
}
fetchData();

Rust: Rust doesn’t have exceptions in the traditional sense. It handles recoverable errors using the Result<T, E> enum and unrecoverable errors using panic!.

  • Result<T, E>: An enum with two variants:
    • Ok(T): Represents success, containing the successful value T.
    • Err(E): Represents failure, containing an error value E.
  • Option<T>: An enum with two variants:
    • Some(T): Represents a value is present, containing the value T.
    • None: Represents a lack of value.

These enums force you to consider and handle potential failure cases explicitly, leading to more robust code.

// Function that might return an error (using Result)
fn divide_rust(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err(String::from("Cannot divide by zero."))
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    // Handling Result with a `match` expression
    match divide_rust(10.0, 2.0) {
        Ok(result) => println!("Result: {}", result), // Result: 5
        Err(e) => println!("Error: {}", e),
    }

    match divide_rust(10.0, 0.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e), // Error: Cannot divide by zero.
    }

    // Using `unwrap()` (panics on Err - generally avoid in production)
    // let safe_result = divide_rust(10.0, 2.0).unwrap(); // Ok(5.0) -> 5.0
    // let dangerous_result = divide_rust(10.0, 0.0).unwrap(); // Panics!

    // Using `expect()` (panics on Err with a custom message)
    // let safe_result = divide_rust(10.0, 2.0).expect("Division should work");
    // let dangerous_result = divide_rust(10.0, 0.0).expect("Division by zero!"); // Panics with "Division by zero!"

    // The `?` operator (for propagating errors)
    // This is similar to `throw`ing an error up the call stack
    fn get_positive_number(input: &str) -> Result<i32, String> {
        let num: i32 = input.parse().map_err(|_| String::from("Invalid number"))?; // Convert parse error to String
        if num < 0 {
            Err(String::from("Number must be positive."))
        } else {
            Ok(num)
        }
    }

    match get_positive_number("123") {
        Ok(n) => println!("Got positive number: {}", n), // Got positive number: 123
        Err(e) => println!("Error: {}", e),
    }

    match get_positive_number("-5") {
        Ok(n) => println!("Got positive number: {}", n),
        Err(e) => println!("Error: {}", e), // Error: Number must be positive.
    }

    match get_positive_number("abc") {
        Ok(n) => println!("Got positive number: {}", n),
        Err(e) => println!("Error: {}", e), // Error: Invalid number
    }

    // Option enum for representing presence or absence of a value
    fn find_first_vowel(s: &str) -> Option<char> {
        for c in s.chars() {
            if "aeiouAEIOU".contains(c) {
                return Some(c);
            }
        }
        None
    }

    match find_first_vowel("hello") {
        Some(vowel) => println!("Found first vowel: {}", vowel), // Found first vowel: e
        None => println!("No vowel found."),
    }

    match find_first_vowel("rhythm") {
        Some(vowel) => println!("Found first vowel: {}", vowel),
        None => println!("No vowel found."), // No vowel found.
    }
}

7. Ownership, Borrowing, and Lifetimes (Rust Specific)

This is perhaps the most fundamental and unique concept in Rust, and it’s crucial for understanding how Rust achieves memory safety without a garbage collector. It directly replaces the garbage collector in JavaScript’s memory management model.

Ownership

Every value in Rust has a variable that’s called its owner. There can only be one owner at a time. When the owner goes out of scope, the value will be dropped (memory freed).

JavaScript Analogy: In JavaScript, memory is automatically managed by a garbage collector. When an object is no longer reachable by any active part of the program, the garbage collector eventually reclaims its memory. You don’t explicitly think about ownership.

let data = [1, 2, 3]; // 'data' owns the array.
let copy = data; // 'copy' also refers to the same array. Both 'data' and 'copy' can access it.

data = null; // The array is still accessible via 'copy'. Garbage collector won't clean it up yet.
console.log(copy); // [1, 2, 3]

Rust: When ownership is transferred, the old variable can no longer be used. This prevents double-frees and data races at compile time.

fn main() {
    let s1 = String::from("hello"); // s1 owns "hello"

    // This is a 'move' operation for String (heap-allocated data)
    let s2 = s1; // s1's ownership of the data moves to s2.
                 // s1 is no longer valid here.

    // println!("{}", s1); // Compile-time error: borrow of moved value: `s1`
    println!("{}", s2); // hello (s2 is the new owner)

    // For primitive types (like integers) or types that implement the `Copy` trait,
    // a 'copy' happens instead of a 'move'.
    let x = 5; // x owns 5
    let y = x; // y gets a copy of 5. x is still valid.

    println!("x: {}, y: {}", x, y); // x: 5, y: 5

    // Ownership in function calls
    let my_string = String::from("ownership rules!");
    takes_ownership(my_string); // my_string moves into the function.
                               // my_string is now invalid here.

    // println!("{}", my_string); // Compile-time error

    let my_int = 100;
    makes_copy(my_int); // my_int is copied into the function.
                        // my_int is still valid.
    println!("My int after copy: {}", my_int); // My int after copy: 100
}

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // some_string goes out of scope and `drop` is called. Memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // some_integer goes out of scope. Nothing special happens.

Borrowing (References)

If you want to use a value without taking ownership, you can borrow it using references (&). References are immutable by default (&T). To modify a borrowed value, you need a mutable reference (&mut T).

JavaScript Analogy: Passing objects by reference to functions. Any changes made to the object inside the function affect the original object outside.

function modifyArrayJS(arr) {
    arr.push(4);
}

let originalArr = [1, 2, 3];
modifyArrayJS(originalArr);
console.log(originalArr); // [1, 2, 3, 4] (original array was modified)

Rust: Rust has strict rules for references to prevent data races and dangling pointers:

  1. You can have multiple immutable references (&T) to a piece of data.
  2. You can have only one mutable reference (&mut T) to a piece of data at any given time.
  3. You cannot have a mutable reference and any immutable references simultaneously.
fn calculate_length(s: &String) -> usize { // `s` is a reference to a String
    s.len()
} // `s` goes out of scope. The value it points to is NOT dropped.

fn change_string(s: &mut String) { // `s` is a mutable reference
    s.push_str(" world");
}

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1); // Pass a reference. Ownership of s1 is not moved.
    println!("The length of '{}' is {}.", s1, len); // s1 is still valid here.

    let mut s2 = String::from("hello");
    change_string(&mut s2); // Pass a mutable reference
    println!("Modified string: {}", s2); // hello world

    // Borrowing rules in action:
    let mut data = String::from("data");
    let r1 = &data; // Immutable reference 1
    let r2 = &data; // Immutable reference 2 (OK)
    println!("r1: {}, r2: {}", r1, r2); // Can use r1 and r2 here

    // Now, let's try a mutable reference:
    // let r3 = &mut data; // ERROR: cannot borrow `data` as mutable because it is also borrowed as immutable
    // After r1 and r2 are no longer used (e.g., after the println!), a mutable reference is allowed:
    let r3 = &mut data; // OK, because r1 and r2 are not used anymore after the previous line
    r3.push_str(" changed");
    println!("r3: {}", r3); // data changed

    // Another example:
    let mut val = 10;
    let ref1 = &val;
    // let ref_mut = &mut val; // ERROR: cannot borrow `val` as mutable because it is also borrowed as immutable
    println!("{}", ref1); // Using ref1, so ref_mut is disallowed above
    let ref_mut = &mut val; // OK here, because ref1 is no longer used
    *ref_mut += 1; // Dereference to modify the value
    println!("Modified val: {}", val); // Modified val: 11
}

Lifetimes

Lifetimes are a core concept in Rust’s borrowing system. They ensure that references never outlive the data they point to, preventing dangling references. Rust’s compiler typically infers lifetimes, but you may need to explicitly annotate them in function signatures or struct definitions, especially when dealing with references in structs or multiple input references.

JavaScript Analogy: JavaScript doesn’t have explicit lifetime concepts because of garbage collection. You generally don’t worry about whether a reference might point to deallocated memory.

Rust: Lifetimes are compile-time concepts that describe how long a reference is valid. They look like 'a, 'b, etc.

// This function takes two string slices and returns one that lives as long as the shortest input.
// The `'a` is a lifetime annotation, ensuring the returned reference is valid.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string is long");
    let result;
    { // Inner scope for string2
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
        // string2 goes out of scope here.
        // If `result` held a reference to `string2`, it would be a dangling pointer.
        // The compiler enforces that `result`'s lifetime is at least as long as `string2`'s (if it were returned)
        // In this case, `result` holds a reference to `string1` which outlives this inner scope.
    }
    println!("The longest string is {}", result); // The longest string is long string is long
}

// Example where explicit lifetimes are needed in a struct
// A struct holding a reference must specify the lifetime of that reference
struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main_excerpt() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
    println!("Important excerpt: {}", i.part);
}

Understanding ownership, borrowing, and lifetimes is the biggest hurdle for JavaScript developers in Rust, but it’s also what gives Rust its powerful safety guarantees.


8. Modules and Crates

JavaScript: JavaScript uses modules (import/export) to organize code and prevent global namespace pollution. npm packages are essentially collections of modules.

// math.js
export function add(a, b) {
    return a + b;
}

export const PI = 3.14159;

// main.js
import { add, PI } from './math.js';
// import * as MathUtils from './math.js'; // Import all

console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

Rust: Rust uses a module system to organize code within a crate (Rust’s compilation unit).

  • Crate: A binary (executable) or a library.
  • Module: A way to organize code within a crate, similar to namespaces or files in other languages.
// src/main.rs (or src/lib.rs for a library crate)

// Declare a module named `math`
mod math {
    pub fn add(a: i32, b: i32) -> i32 { // `pub` makes it public
        a + b
    }

    pub const PI: f64 = 3.14159;

    pub mod calculations { // Nested module
        pub fn multiply(a: i32, b: i32) -> i32 {
            a * b
        }
    }

    // Private function within the module
    fn private_helper() {
        println!("This is a private helper.");
    }
}

// Another module in the same file
mod greetings {
    pub fn hello(name: &str) {
        println!("Hello, {}!", name);
    }
}

fn main() {
    // Accessing items from modules using their full path
    println!("Sum: {}", crate::math::add(10, 20)); // Sum: 30
    println!("PI: {}", math::PI); // `crate::` is implicit for top-level modules
    println!("Product: {}", math::calculations::multiply(5, 4)); // Product: 20

    // Use `use` keyword to bring items into scope for easier access
    use crate::math::add;
    use crate::math::PI;
    use math::calculations::multiply; // Can also omit `crate::`

    println!("Sum (with use): {}", add(1, 2)); // Sum (with use): 3
    println!("PI (with use): {}", PI); // PI (with use): 3.14159
    println!("Product (with use): {}", multiply(2, 6)); // Product (with use): 12

    greetings::hello("Rustacean"); // Hello, Rustacean!
}

// For larger projects, modules are typically split into separate files:
// src/main.rs
// src/math.rs
// src/math/calculations.rs
// src/greetings.rs

// If `math` module is in `src/math.rs`:
// In src/main.rs:
// mod math; // This tells Rust to look for `src/math.rs` or `src/math/mod.rs`
// then access `math::add` etc.

9. Concurrency

JavaScript: JavaScript is single-threaded by nature. Concurrency is achieved through an event loop, asynchronous operations (Promises, async/await), and Web Workers (for true parallelism in the browser).

// Asynchronous operation (Promise)
function simulateAsyncTask(duration) {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`Task finished after ${duration / 1000} seconds.`);
            resolve();
        }, duration);
    });
}

console.log("Start JS tasks.");
simulateAsyncTask(2000); // Runs in the background
console.log("More JS code runs immediately.");

// Using async/await
async function runAsync() {
    console.log("Async function started.");
    await simulateAsyncTask(1000); // Await pauses execution of *this* function
    console.log("Async function finished.");
}
runAsync();

// Web Workers (for CPU-bound tasks in browsers)
// const worker = new Worker('worker.js');
// worker.postMessage('start calculation');
// worker.onmessage = (e) => console.log('Worker finished:', e.data);

Rust: Rust provides robust concurrency primitives, primarily OS threads, and safe ways to share data between them. The ownership system is critical for preventing data races and deadlocks at compile time.

use std::thread;
use std::time::Duration;
use std::sync::{mpsc, Arc, Mutex}; // For message passing and shared state

fn main() {
    // 1. Spawning new threads
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("Hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..3 {
        println!("Hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap(); // Wait for the spawned thread to finish
    println!("Main thread finished.");

    // 2. Message Passing (Channels) - like Web Workers or Node.js Worker Threads communication
    let (tx, rx) = mpsc::channel(); // Transmitter and Receiver

    thread::spawn(move || {
        let val = String::from("hi from channel thread");
        tx.send(val).unwrap(); // Send the value
        // val is moved here, cannot be used anymore:
        // println!("val is {}", val); // Compile-time error
    });

    let received = rx.recv().unwrap(); // Block until a value is received
    println!("Got: {}", received); // Got: hi from channel thread

    // 3. Shared State Concurrency (Mutex and Arc) - for truly shared, mutable data
    // Arc (Atomic Reference Counted) allows multiple threads to own a pointer to the same data on the heap.
    // Mutex provides mutual exclusion (only one thread can access the data at a time).
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter); // Clone the Arc, not the inner Mutex
        let handle = thread::spawn(move || {
            let mut num = counter_clone.lock().unwrap(); // Acquire the lock
            *num += 1; // Dereference and modify
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap()); // Result: 10
}

Rust’s async/await syntax for asynchronous programming is also available via external crates like tokio or async-std, providing an event-loop model similar to JavaScript but with Rust’s strong type and safety guarantees.


10. Package Management (npm vs. Cargo)

JavaScript:npm (Node Package Manager) is the default package manager for Node.js and JavaScript. It handles dependencies, scripts, and publishing packages.

  • package.json: Manifest file describing the project and its dependencies.
  • npm install: Installs dependencies.
  • npm run <script>: Runs scripts defined in package.json.
  • npm publish: Publishes a package to the npm registry.

Rust:Cargo is Rust’s build system and package manager. It handles creating projects, managing dependencies, compiling code, running tests, and publishing crates to crates.io.

  • Cargo.toml: The manifest file for a Rust project (crate). It contains metadata, dependencies, and build configurations.
  • cargo new <project_name>: Creates a new Rust project.
  • cargo build: Compiles the project and its dependencies.
  • cargo run: Compiles and runs the project.
  • cargo test: Runs tests.
  • cargo add <crate_name>: Adds a dependency (Cargo feature introduced later).
  • cargo publish: Publishes the crate to crates.io.

Example Cargo.toml:

# Cargo.toml
[package]
name = "my_rust_app"
version = "0.1.0"
edition = "2021"

[dependencies]
# Example of adding a dependency (e.g., for asynchronous operations)
tokio = { version = "1", features = ["full"] }
rand = "0.8" # For random number generation

Cargo is one of the most loved features of Rust, providing a consistent and efficient development workflow.


11. Conclusion

Transitioning from JavaScript to Rust is a significant but rewarding journey. While JavaScript offers dynamic flexibility and rapid development, Rust provides unparalleled performance, memory safety, and concurrency guarantees, making it suitable for critical systems.

Key takeaways for JavaScript developers:

  • Static vs. Dynamic Typing: Rust requires explicit types or relies on strong inference, enforcing type safety at compile time, unlike JavaScript’s runtime type checking.
  • Memory Management: Abandon the garbage collector for Rust’s ownership, borrowing, and lifetime system. This is the steepest learning curve but the source of Rust’s safety.
  • Mutability: Rust defaults to immutability, requiring mut for changes, promoting safer, more predictable code.
  • Error Handling: Embrace Result and Option enums for explicit, robust error handling instead of try/catch exceptions.
  • Ecosystem: Cargo is as central to Rust as npm is to JavaScript, providing a seamless build and package management experience.
  • Concurrency: Rust’s type system makes concurrent programming much safer, preventing common pitfalls like data races at compile time.

By understanding these core differences and embracing Rust’s unique paradigms, you’ll find that many familiar programming concepts have powerful, safe equivalents in Rust. Happy coding!


I hope this comprehensive comparison in Markdown format is exactly what you were looking for! You can now paste this into Obsidian (or any Markdown editor that supports PDF export) and generate your PDF.

Please let me know if you’d like any specific section expanded, clarified, or if there’s anything else you’d like to add!