Unveiling Rust: A JavaScript Developer’s Guide to Mastering a New Realm

Igor Komolov
5 min readDec 3, 2023

--

Introduction: Embarking on a Rusty Adventure

Welcome, JavaScript enthusiasts! Are you ready to diversify your programming skills and dive into the world of Rust?

This journey promises to be both enlightening and challenging. As a seasoned JavaScript developer, you’re already equipped with a strong foundation in programming, which will be invaluable as you explore Rust. In this blog, I am going to walk through the key aspects of Rust and how they compare and contrast with your JavaScript knowledge.

Feel free to leave comments below, would love to hear your feedback!

Meet Cargo: Rust’s Build System and Package Manager

Rust developers rely on Cargo, a tool similar in spirit to npm in the JavaScript ecosystem. Cargo simplifies dependency management, compilation, and more, making your development process smoother.

JavaScript (using npm)

npm init
npm install express

Rust (using Cargo)

cargo new my_project
cd my_project
cargo build

Understanding Rust’s Ownership Model: A Paradigm Shift

One of the most striking differences you’ll encounter in Rust is its unique approach to memory management. Unlike JavaScript, which relies on garbage collection (which is automatic and determined by the engine), Rust uses an ownership model for memory safety. This means that each piece of data has a single owner responsible for its disposal. It’s a different mindset but one that greatly minimizes memory-related bugs.

JavaScript:

let objectA = { value: 10 };
let objectB = objectA; // Reference copy
objectB.value = 20;
console.log(objectA.value); // Outputs: 20

Rust:

let string_a = String::from("Hello");
let string_b = string_a; // Ownership is moved
// println!("{}", string_a); // This line would cause a compile-time error
println!("{}", string_b); // Outputs: Hello

In Rust, string_a's ownership is moved to string_b, and string_a is no longer valid. This prevents issues like double free errors.

Zero-Cost Abstractions: Efficiency Meets High-Level Programming

Rust’s zero-cost abstractions are a game-changer. They allow you to write high-level, abstract code without sacrificing performance. This concept might remind you of JavaScript’s modern frameworks and libraries that hide complexity while optimizing runtime efficiency.

This concept is more about Rust’s overall design philosophy and doesn’t lend itself to a direct code comparison. Rust’s iterators and trait-based generics are examples of zero-cost abstractions.

Concurrency Made Safe: Eliminating Data Races

Rust shines in its ability to handle concurrent programming safely. Thanks to its ownership rules, data races become a thing of the past. This is a significant leap from JavaScript, where concurrency often requires careful handling of callbacks and promises.

JavaScript (using Promises)

let sharedData = 0;

async function increment() {
sharedData += 1;
}

async function main() {
await Promise.all([increment(), increment()]);
console.log(sharedData); // Might not be 2 due to race conditions
}

main();

Rust (using thread-safe mechanisms)

use std::sync::{Arc, Mutex};
use std::thread;

let shared_data = Arc::new(Mutex::new(0));

let thread1_data = Arc::clone(&shared_data);
let thread2_data = Arc::clone(&shared_data);

let thread1 = thread::spawn(move || {
let mut num = thread1_data.lock().unwrap();
*num += 1;
});

let thread2 = thread::spawn(move || {
let mut num = thread2_data.lock().unwrap();
*num += 1;
});

thread1.join().unwrap();
thread2.join().unwrap();

println!("{}", *shared_data.lock().unwrap()); // Outputs: 2

Rust’s Arc and Mutex ensure thread-safe access to shared data, eliminating race conditions.

The Power of Rust’s Type System: From Dynamic to Strong Typing

Coming from JavaScript’s loosely-typed nature (unless you are already using TypeScript, and you should be!), Rust’s strong typing system might seem daunting at first. However, Rust also features type inference, reducing the need for explicit type declarations. This blend of strong typing and type inference strikes a balance between safety and developer convenience.

JavaScript

function add(a, b) {
return a + b;
}
console.log(add(5, 10)); // Outputs: 15
console.log(add("5", "10")); // Outputs: "510"

Rust

fn add(a: i32, b: i32) -> i32 {
a + b
}
println!("{}", add(5, 10)); // Outputs: 15
// println!("{}", add("5", "10")); // This line would cause a compile-time error

Rust requires explicit types for function arguments, providing compile-time type safety.

Embracing Immutability: A New Default

In Rust, variables are immutable by default, contrasting with JavaScript’s mutable let declarations. This fosters a coding style that is more predictable and easier to debug.

JavaScript

let x = 10;
x = 20; // Allowed

Rust

let x = 10;
// x = 20; // This line would cause a compile-time error, as x is immutable by default

In Rust, variables are immutable by default, enforcing safer and more predictable code.

Pattern Matching: Beyond Switch Statements

Rust takes the concept of switch statements to the next level with pattern matching. This feature is not just a control flow tool but also a powerful mechanism for deconstructing complex data structures.

JavaScript (using switch)

let fruit = "apple";

switch (fruit) {
case "apple":
console.log("Apple");
break;
case "banana":
console.log("Banana");
break;
default:
console.log("Unknown fruit");
}

Rust (using match)

let fruit = "apple";

match fruit {
"apple" => println!("Apple"),
"banana" => println!("Banana"),
_ => println!("Unknown fruit"),
}

Rust’s match is more powerful and expressive than JavaScript's switch.

Rust’s Modular Approach: Crates and Modules

Like JavaScript’s packages, Rust organizes code into crates and modules. This system is a familiar territory for JavaScript developers and helps in maintaining large codebases.

JavaScript (using modules)

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

// main.js
import { add } from './utils.js';
console.log(add(5, 10)); // Outputs: 15

Rust

// utils.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

// main.rs
mod utils;
use utils::add;
fn main() {
println!("{}", add(5, 10)); // Outputs: 15
}

Both languages offer a modular approach, but Rust uses a combination of modules and crates.

Rethinking Error Handling

Rust’s approach to error handling, with its Result<T, E> and Option<T> types, encourages explicit and robust error management, a departure from JavaScript's error handling practices.

JavaScript (using try-catch)

function mightFail() {
if (Math.random() < 0.5) {
throw new Error("Failed!");
}
return "Success!";
}

try {
console.log(mightFail());
} catch (error) {
console.error(error.message);
}

Rust (using Result)

fn might_fail() -> Result<&'static str, &'static str> {
if rand::random() {
Ok("Success!")
} else {
Err("Failed!")
}
}

match might_fail() {
Ok(value) => println!("{}", value),
Err(error) => println!("Error: {}", error),
}

Rust’s Result type is a more explicit way of handling errors compared to JavaScript’s try-catch.

Direct Hardware Access: Tapping into Systems Programming

Rust offers a level of systems-level control akin to languages like C or C++, but with added safety checks. This is a significant shift from the high-level nature of JavaScript and requires a seperate deep dive for later.

Asynchronous Programming in Rust: A Familiar Concept

If you’re accustomed to JavaScript’s async/await, Rust’s asynchronous programming will feel familiar. It allows for efficient, non-blocking code execution, a critical feature in modern software development.

JavaScript (using ‘async/await')

async function fetchData() {
let response = await fetch("https://api.igorkomolov.com/data");
let data = await response.json();
console.log(data);
}

fetchData();

Rust (using ‘async/await’)

async fn fetch_data() {
let response = reqwest::get("https://api.igorkomolov.com/data").await.unwrap();
let data = response.text().await.unwrap();
println!("{}", data);
}

#[tokio::main]
async fn main() {
fetch_data().await;
}

Both languages support asynchronous programming with a similar syntax, although Rust requires an async runtime like Tokio.

Conclusion: Your Rust Journey Begins

Transitioning from JavaScript to Rust is an exciting and rewarding endeavour. While Rust’s strict type system and ownership rules present a learning curve, they also open up a world of possibilities in terms of performance and safety.

The Rust community is known for its welcoming and supportive nature, so don’t hesitate to reach out for help and resources. Your journey into Rust is not just about learning a new language; it’s about expanding your horizons as a developer. Happy coding!

--

--

Igor Komolov
Igor Komolov

Written by Igor Komolov

Founder & CTO who writes code, day trades, cycles, golfs, takes pictures, makes art and reads ALOT.

No responses yet