Diving Headfirst Into Rust: A Node.js Developer’s Rollercoaster Ride

Embracing the World of Rust

Giuseppe Albrizio
Better Programming

--

Have you ever felt that irresistible urge to push yourself beyond your comfort zone and dive into the unknown? That’s precisely the sensation that washed over me when I took the leap from the cozy realm of Node.js to the captivating universe of Rust.

As an experienced Node.js developer, I yearned to elevate my skill set and delve into a language that’s been causing a stir in the programming sphere with its lightning-fast performance and top-notch safety features. Now, I’m excited to recount my initial adventures as I fasten my seatbelt and embark on this thrilling expedition.

Why Rust?

The first question that pops up in your mind might be, “Why Rust? What’s so special about it?”

Rust has been consistently ranked as the “most loved language” in the Stack Overflow Developer Survey. Its strong type system, memory safety, and C-like performance make it a great fit for systems programming, WebAssembly, and even game development.

Intrigued? I sure was!

The Learning Curve

As I ventured into Rust territory, I quickly realized that it was not a walk in the park. The learning curve was steeper than I had anticipated, and some Rust concepts left me scratching my head. However, I knew that persistence would be key to overcoming these challenges.

The Ownership System

One of Rust’s most distinctive features is its own system, which manages memory safety without a garbage collector. Coming from Node.js, where memory management is mostly abstracted away, this was a paradigm shift for me.

In Rust, each value has a single owner, and when that owner goes out of scope, the value is automatically deallocated.

fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is now invalidated, and s2 takes ownership
}

Pattern Matching

Rust’s pattern matching is another powerful feature that took some time for me to fully grasp. Unlike JavaScript’s switch statement, Rust's match statement allows you to compare values against various patterns and execute code accordingly.

This enables more expressive and concise code, with better error handling.

fn main() {
let x = 1;

match x {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
_ => println!("Anything else"),
}
}

The Borrow Checker

Rust’s borrow checker is like a strict school teacher, constantly monitoring your code to make sure you’re not breaking any rules.

While it can be frustrating at times (especially when it refuses to compile your code), it’s a necessary evil that ensures memory safety and prevents data races.

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

let r1 = &mut s;
let r2 = &mut s; // This line will cause a compile-time error

println!("{}, {}", r1, r2);
}

Error Handling

Error handling in Rust is quite different from Node.js. While JavaScript relies on exceptions and the try-catch block, Rust uses the Result enum for functions that can return an error. Let's look at a simple example of reading a file in both languages.

The Node.js way:

const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('An error occurred:', err);
return;
}
console.log('File contents:', data);
});

The Rust way:

use std::fs::File;
use std::io::prelude::*;

fn main() {
let mut file = match File::open("example.txt") {
Ok(file) => file,
Err(error) => {
println!("An error occurred: {:?}", error);
return;
}
};

let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => println!("File contents: {}", contents),
Err(error) => println!("An error occurred: {:?}", error),
}
}

In the Rust example, we use match statements to handle the Result returned by the File::open() and read_to_string() functions. This approach encourages more explicit error handling and makes it less likely to forget to handle errors.

Concurrency

Concurrency is an important aspect of modern programming. While Node.js is single-threaded and relies on the event loop and asynchronous programming to achieve concurrency, Rust provides powerful abstractions for multi-threading.

Let’s compare a simple example of running two concurrent tasks in both languages.

The Node.js way:

const task1 = () => {
console.log('Starting task 1');
setTimeout(() => {
console.log('Finished task 1');
}, 1000);
};

const task2 = () => {
console.log('Starting task 2');
setTimeout(() => {
console.log('Finished task 2');
}, 1000);
};

task1();
task2();

The Rust way:

use std::thread;
use std::time::Duration;

fn main() {
let handle1 = thread::spawn(|| {
println!("Starting task 1");
thread::sleep(Duration::from_secs(1));
println!("Finished task 1");
});

let handle2 = thread::spawn(|| {
println!("Starting task 2");
thread::sleep(Duration::from_secs(1));
println!("Finished task 2");
});

handle1.join().unwrap();
handle2.join().unwrap();
}

In the Rust example, we harness the power of the thread::spawn() function to craft two threads that run side by side. Following that, we employ the join() method to patiently await the completion of both tasks. This scenario shines a light on Rust's remarkable capability to leverage multi-core processors, delivering a more efficient concurrency solution in contrast to Node.js' single-threaded approach. These examples bring to the fore some of the critical distinctions between Node.js and Rust, underscoring Rust's commitment to safety, unambiguous error handling, and robust concurrency tools.

Embracing the Challenge

Taking the leap from Node.js to Rust has been a thrilling rollercoaster ride, complete with a mix of bafflement, exasperation, and ultimately, sweet victory. While it’s tough to step away from the familiar, there’s a profound sense of satisfaction in mastering new concepts and expanding one’s perspective as a developer. In the forthcoming articles, I’ll dive even deeper into Rust’s captivating realm, sharing more tales and revelations as I keep exploring this enthralling language. So, stay tuned, and don’t hesitate to recount your own Rust adventures in the comments below!

--

--

Graduated in sound engineering and working as full-stack developer. I’ve spent these years in writing tons of line of codes and learning new things every day.