Blogs/Technology

5 Rust Features I Wish JavaScript and Ruby Had

Written byDevesh Mhatre
Aug 6, 2026
14 Min Read
5 Rust Features I Wish JavaScript and Ruby Had Hero
Too Long? Read This First

- cargo doc --open generates version-matched documentation for a Rust project and its dependencies.
- Rust variables are immutable unless the developer explicitly adds mut.
- Shadowing lets developers transform a value and reuse its name, even when its type changes.
- Safe Rust uses Option<T> instead of nullable references, making missing values explicit.
- Rust applications compile into native binaries that do not require Rust to be installed on the target machine.
- These advantages come with stricter compiler rules and a steeper initial learning curve.
- JavaScript and Ruby remain more practical for many web products where development speed and ecosystem maturity matter most.

I started looking at Rust while experimenting with a small application for a Raspberry Pi with just 512 MB of memory. My first instinct was to use something familiar: Node.js, Ruby, or perhaps SvelteKit. But on hardware that constrained, runtime size, memory use, and deployment overhead stop being theoretical concerns.

That experiment made me examine Rust from the perspective of someone who spends most of his time writing JavaScript and Ruby. I had previously explored C, but I had only been learning Rust for around three months when several of its design choices began changing how I thought about everyday development.

This is not an argument that Rust should replace JavaScript or Ruby. All three languages solve different problems well. These are simply five Rust features, and, in one case, a Cargo feature, that I often miss when returning to dynamic languages.

1. Local, Version-Matched Documentation With cargo doc --open

Cargo is Rust’s package manager and build system. It downloads dependencies, compiles packages, runs tests, creates distributable packages, and publishes libraries to crates.io.

One Cargo command made an immediate impression on me:

cargo doc --open

The command generates HTML documentation for the current project and its dependencies, stores it under target/doc, and opens it in the default browser.

If a project uses an older dependency, the generated documentation corresponds to the version installed in that project. That detail matters more than it might initially appear.

Why version-matched documentation matters

I once worked on a React project created in 2022 that still depended on @reach/router. React Router was eventually folded into the React Router project, leaving older documentation increasingly difficult to find.

I was trying to determine whether its internal Link component could open a destination in a new tab. Search results kept taking me to newer React Router documentation, forum discussions, and outdated examples. After spending considerable time searching, I discovered that the version we used did not support what I needed in the way I expected.

This is a common problem in mature projects. The documentation ranking highest in search results may describe the latest release, while the application may depend on a version that is several years old.

With Rust, running:

cargo doc --open

creates documentation for the exact crate versions resolved in the project. It includes the project’s public API and, by default, documentation for its dependencies.

The official Cargo reference confirms that --open opens the generated documentation after building it. The resulting files are available locally, so they remain accessible even when a crate is old, archived, or no longer easy to find online. Cargo documentation

You can exclude dependency documentation when you only need documentation for your own crate:

cargo doc --no-deps --open

To include private functions, structs, and modules from the current package, use:

cargo doc --document-private-items --open

For locally installed Rust standard-library documentation, you can run:

rustup doc

How JavaScript and Ruby compare

JavaScript projects can generate API documentation using JSDoc or TypeDoc, but these tools generally require project-specific configuration. They do not automatically provide one unified, local documentation site covering the application and every installed dependency.

TypeScript’s editor integration provides excellent type information, signatures, and source navigation. However, its usefulness depends on the quality of the type declarations and documentation published by each package.

Ruby has strong documentation tools as well. RDoc and YARD can generate project documentation, while ri can display documentation for installed Ruby classes, methods, and gems from the terminal. gem server can also expose installed gem documentation through a local web server.

The difference is integration. Cargo makes documentation generation a normal, predictable part of the project toolchain:

cargo doc --open

There is no need to locate the correct external documentation site or wonder whether the page describes the version currently installed.

Where this feature is especially useful

Local dependency documentation becomes valuable when:

  • Maintaining long-lived applications
  • Working in restricted or offline environments
  • Auditing unfamiliar dependencies
  • Upgrading between major library versions
  • Debugging an application with an old lockfile
  • Exploring a new Rust codebase without leaving the editor and terminal

It is a small feature, but it eliminates a surprisingly frequent interruption in development.

2. Variables Are Immutable by Default

A Rust variable cannot be reassigned unless it is explicitly declared as mutable.

fn main() {
    let attempts = 1;
    attempts = 2;
}

The compiler rejects this code because attempts is immutable.

To allow reassignment, the developer must add mut:

fn main() {
    let mut attempts = 1;
    attempts = 2;

    println!("Attempts: {attempts}");
}

The important part is not that Rust supports immutable variables. JavaScript and Ruby can both represent values that should not change. The difference is that Rust makes immutability the default and mutation an explicit decision.

Why this matters in real applications

Mutable state increases the number of possible states a program can enter.

Consider a variable holding an application configuration. If any function can modify it, understanding its value at a particular point requires knowing every function that may have run before it.

If the value is immutable, that uncertainty disappears. Once assigned, it remains the same for the rest of its scope.

This makes code easier to reason about, particularly when:

  • Multiple functions share data
  • Tasks run concurrently
  • A value passes through several transformations
  • Refactoring changes execution order
  • State-related bugs are difficult to reproduce

Immutability does not automatically make concurrent code safe. Rust’s ownership, borrowing, and thread-safety rules do much more of that work. However, preventing unplanned mutation removes one common source of complexity.

JavaScript’s const is close, but not identical

JavaScript developers can use const to prevent a binding from being reassigned:

const settings = {
  theme: "dark"
};

// Not allowed:
// settings = { theme: "light" };

// Still allowed:
settings.theme = "light";

const protects the binding, not the object stored behind it. The object’s properties can still change unless additional measures such as Object.freeze() are used.

const settings = Object.freeze({
  theme: "dark"
});

Even then, Object.freeze() is shallow unless nested values are frozen separately.

Rust also distinguishes between an immutable binding and the mutability of the value it refers to, but its type system and borrowing rules make those relationships much more explicit.

Ruby takes a different approach

Ruby variables are mutable by convention, and objects can usually be modified:

settings = { theme: "dark" }
settings[:theme] = "light"

Ruby provides freeze for preventing changes to an object:

settings = { theme: "dark" }.freeze

However, immutability is something Ruby developers opt into. Rust reverses that decision: mutation must be justified explicitly with mut.

Is immutability by default always better?

Not necessarily.

JavaScript and Ruby are approachable partly because developers can write and modify state without first learning an ownership model. This flexibility is useful for scripting, prototypes, web applications, and rapidly changing products.

Rust trades some of that freedom for compile-time guarantees. The result is often more predictable code, but the developer must satisfy stricter rules before the program runs.

Let’s Develop Your JavaScript Project Together!

We build fast, reliable, and scalable JavaScript applications that power modern businesses across the web.

I would still like JavaScript and Ruby projects to use immutable patterns more consistently. But making immutability a new language-wide default would break enormous amounts of existing code and conflict with how their ecosystems have evolved.

The feature is valuable precisely because Rust was designed around it from the beginning.

3. Predictable Variable Shadowing

Variable shadowing means declaring a new variable with the same name as an existing variable. The new declaration hides, or shadows, the previous one within that scope.

Rust permits this even within the same block:

fn main() {
    let user_input = " 42 ";
    let user_input = user_input.trim();
    let user_input = user_input.parse::<u32>().unwrap();

    println!("Parsed value: {user_input}");
}

Three different variables are created:

  1. user_input initially holds a string containing whitespace.
  2. The next user_input holds a trimmed string slice.
  3. The final user_input holds an unsigned integer.

The variable name stays aligned with its role, while the type changes as the data moves through the processing pipeline.

The official Rust Book explains that shadowing creates a new variable rather than mutating the previous one. Because it is a new binding, its type can also change.

Shadowing is not the same as mut

A mutable variable can change its value, but it must retain a compatible type:

fn main() {
    let mut spaces = "   ";

    // Compilation error:
    // spaces = spaces.len();
}

spaces begins as a string slice. Assigning an integer to the same mutable variable is not allowed.

Shadowing creates a new variable, so the type can change:

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();

    println!("{spaces}");
}

The distinction is useful:

  • Use mut when one variable’s value genuinely changes over time.
  • Use shadowing when data is being transformed into a new representation.

How JavaScript handles it

JavaScript supports shadowing across nested scopes:

const value = "42";

function parseValue() {
  const value = 42;
  console.log(value);
}

parseValue();
console.log(value);

However, JavaScript does not allow a let or const binding to be redeclared in the same scope:

let value = "42";

// SyntaxError in the same scope:
let value = Number(value);

Developers normally introduce a new name:

const value = "42";
const parsedValue = Number(value);

That is often clearer, but transformation-heavy functions can gradually accumulate names such as:

const rawValue = " 42 ";
const trimmedValue = rawValue.trim();
const parsedValue = Number(trimmedValue);
const validatedValue = validate(parsedValue);

Rust shadowing allows each completed transformation to take ownership of the simplest relevant name.

How Ruby handles it

Ruby allows reassignment:

value = "42"
value = value.to_i

But this is mutation of the local variable, not Rust-style shadowing. The binding now refers to a different object and may hold an entirely different type.

Ruby’s dynamic typing makes this possible, but it also means the language does not provide the same compile-time distinction between transformation and mutation.

Why Rust’s approach feels safer

Rust shadowing communicates two things:

  • The previous representation is no longer needed.
  • The new value is immutable unless explicitly declared otherwise.

That makes it useful for parsing configuration, sanitizing input, decoding API responses, converting units, and refining values after validation.

Shadowing can still be overused. Reusing a name for unrelated concepts makes code difficult to follow. It works best when each value represents a more refined version of the same underlying information.

4. No Nullable References in Safe Rust

Saying “Rust has no null” is convenient, but it needs qualification.

Rust can interact with null raw pointers, particularly in unsafe code and foreign-function interfaces. However, ordinary references in safe Rust cannot be null. When a value may be absent, Rust represents that possibility with Option<T>.

Option<T> is an enum with two variants:

enum Option<T> {
    Some(T),
    None,
}

Some(value) means a value exists. None means it does not.

Here is a simple example:

fn print_discount(discount: Option<u32>) {
    match discount {
        Some(percent) => {
            println!("Discount: {percent}%");
        }
        None => {
            println!("No discount is available");
        }
    }
}

fn main() {
    print_discount(Some(20));
    print_discount(None);
}

The function cannot treat discount as an ordinary integer. It must first account for the possibility that no value exists.

Why Option<T> is valuable

In JavaScript, a missing value might be represented by:

  • null
  • undefined
  • An omitted property
  • An empty string
  • NaN
  • A custom sentinel value

Ruby applications commonly use nil.

These values are easy to pass through several layers of an application before something finally fails:

function formatCustomer(customer) {
  return customer.address.city.toUpperCase();
}

If address is null or missing, the application throws an error at runtime.

Optional chaining makes the code safer:

function formatCustomer(customer) {
  return customer.address?.city?.toUpperCase();
}

But it may also silently return undefined, passing the missing-value problem to the next operation.

Rust requires the absence to appear in the type:

struct Address {
    city: String,
}

struct Customer {
    address: Option<Address>,
}

Code consuming Customer can immediately see that address might not exist.

Handling optional values without a full match

Rust provides several concise ways to work with Option<T>.

Use if let when only the present case matters:

if let Some(address) = customer.address {
    println!("{}", address.city);
}

Use unwrap_or to provide a default:

let retries = configured_retries.unwrap_or(3);

Use map to transform a value only when it exists:

let uppercase_name = name.map(|value| value.to_uppercase());

Use the ? operator to return early when an optional value is missing:

fn find_city(customer: &Customer) -> Option<&str> {
    let address = customer.address.as_ref()?;
    Some(address.city.as_str())
}

Rust also makes match exhaustive. If an enum gains another variant or a case has not been addressed, the compiler can flag the incomplete handling.

JavaScript and Ruby alternatives

TypeScript can express optional and nullable values:

type Customer = {
  address?: {
    city: string;
  };
};

With strictNullChecks enabled, TypeScript provides significantly better protection than plain JavaScript. Libraries such as fp-ts also provide an Option type.

Ruby developers can use patterns such as guard clauses, the safe-navigation operator, Sorbet, or custom result objects:

city = customer.address&.city

These tools help, but neither JavaScript nor Ruby requires every project to model absence explicitly.

For API-heavy applications, that difference becomes important. Third-party payloads frequently contain omitted, nullable, or partially populated fields. An explicit Option<T> forces the parser and the rest of the application to agree on how missing data should be handled.

5. Deployable Binaries Without Installing Rust

This feature became especially relevant during my Raspberry Pi experiment.

Installing the complete Rust toolchain on a device with 512 MB of memory and limited storage was unnecessary. The device only needed to run my application; it did not need Cargo, rustc, source code, or development documentation.

Rust separates those two environments.

You can compile the application on a development or build machine:

cargo build --release

Cargo places the optimized executable under:

target/release/

You can then copy the resulting binary to a compatible target machine and execute it without installing Rust there.

./my_application

How this differs from Node.js and Ruby

A typical Node.js deployment needs:

  • A compatible Node.js runtime
  • The application’s JavaScript files
  • Production dependencies
  • Usually a package manager or prepared node_modules directory

Let’s Develop Your JavaScript Project Together!

We build fast, reliable, and scalable JavaScript applications that power modern businesses across the web.

A Ruby application normally needs:

  • A compatible Ruby interpreter
  • The application source
  • The required gems
  • Bundler or an equivalent dependency setup
  • Native libraries required by some gems

Containers make these dependencies repeatable, but they do not make the runtime disappear. The runtime and libraries are packaged inside the container image.

A Rust application can often be deployed as one executable plus any configuration, templates, certificates, or external assets it needs.

The important portability limitation

A Rust binary is not universally portable.

The build must match the target’s:

  • Operating system
  • Processor architecture
  • Application Binary Interface
  • Required system libraries

A binary compiled for an x86-64 Linux workstation will not run directly on an ARM-based Raspberry Pi. You must compile for the Pi’s target architecture, either on the device or through cross-compilation.

For example, the target might be:

rustup target add aarch64-unknown-linux-gnu

You would then build for that target:

cargo build \
  --release \
  --target aarch64-unknown-linux-gnu

Cross-compilation may also require the correct linker and native libraries. Crates that depend on OpenSSL, database clients, or other C libraries can make the process more involved.

“Standalone binary” should therefore be understood as “no Rust toolchain or language runtime required,” not “guaranteed to run on every computer without dependencies.”

Why this still improves deployment

Once the build pipeline is configured correctly, binary deployment offers useful advantages:

  • Smaller production environments
  • Faster application startup
  • Fewer runtime packages to patch
  • Consistent artifacts between releases
  • Simpler installation on constrained devices
  • Easier container images
  • Reduced dependency drift on production servers

For small command-line tools, background workers, proxies, agents, and edge applications, being able to ship a compiled executable is a substantial advantage.

Rust vs JavaScript vs Ruby: How These Features Compare

CapabilityRustJavaScriptRuby
Default variable behaviorImmutableMutable with let; non-reassignable binding with constMutable
Same-scope shadowingSupported with letNot supported with let or constReassignment rather than typed shadowing
Missing valuesOption<T> in safe codenull, undefined, optional propertiesnil
Compile-time null handlingEnforced through typesAvailable with TypeScript configurationAvailable through optional tooling such as Sorbet
Local dependency documentationIntegrated through CargoDepends on package and documentation toolingAvailable through RI/RDoc, but less unified
Production runtimeNative executableRequires a JavaScript runtimeRequires a Ruby interpreter
Type checkingStatic and compile-timeDynamic; static with TypeScriptDynamic; optional static tooling
Primary strengthSafety, control, and predictable performanceWeb ecosystem and development flexibilityExpressive application development
Default variable behavior
Rust
Immutable
JavaScript
Mutable with let; non-reassignable binding with const
Ruby
Mutable
1 of 8

The table does not identify a universal winner. It shows the trade-offs each language makes.

Features I Would Not Copy Without Their Trade-Offs

It is easy to look at Rust’s guarantees and assume that every language should adopt them. But these features work together as part of Rust’s overall design.

Immutability is supported by ownership and borrowing. Option<T> is practical because enums and pattern matching are deeply integrated into the type system. Standalone binaries are possible because Rust compiles ahead of time.

Adding isolated versions of these features to JavaScript or Ruby would not automatically produce Rust’s guarantees. It could instead add complexity while weakening the flexibility that makes those languages productive.

The more useful lesson is to bring the underlying habits into other ecosystems:

  • Prefer const unless a JavaScript binding must be reassigned.
  • Enable TypeScript’s strict null checking.
  • Use guard clauses and explicit result objects in Ruby.
  • Avoid using null, undefined, empty strings, and missing properties interchangeably.
  • Generate and preserve documentation for the versions used by the project.
  • Keep production runtime dependencies deliberate and reproducible.

Those practices do not turn JavaScript or Ruby into Rust, but they can prevent similar classes of bugs.

When Should You Use Rust, JavaScript, or Ruby?

Rust is a strong candidate when predictable performance, memory control, concurrency safety, small deployments, or native integration is central to the product.

Common examples include:

  • Command-line applications
  • Embedded and edge software
  • Performance-sensitive backend services
  • Databases and storage systems
  • Networking infrastructure
  • Developer tools
  • WebAssembly modules
  • Native extensions for other languages

JavaScript remains the natural choice for browser applications and is highly practical for full-stack products, real-time services, APIs, and teams that benefit from using one language across the frontend and backend.

Ruby, particularly with Rails, remains excellent for business applications where developer productivity, conventions, and rapid iteration matter more than low-level control.

If a growing Node.js application requires additional backend capacity rather than a language rewrite, working with experienced Node.js developers may deliver more value than replacing a suitable stack with Rust solely for theoretical performance gains.

The right question is not “Which language is best?” It is “Which language makes the important constraints of this project easiest to manage?”

Frequently Asked Questions

What makes Rust different from JavaScript and Ruby?

Rust uses static typing, ownership, compile-time checks, and native compilation to prioritize safety and predictable performance. JavaScript and Ruby prioritize flexibility, expressive code, large web ecosystems, and faster initial development.

Why are Rust variables immutable by default?

Default immutability prevents accidental reassignment and makes state changes easier to identify. When a value genuinely needs to change, the developer communicates that intention explicitly by declaring the variable with mut.

Is JavaScript’s const the same as Rust immutability?

Not completely. JavaScript’s const prevents reassignment of a binding, but an object referenced by that binding can still be modified. Rust combines binding rules with stricter ownership and borrowing controls.

What problem does Option<T> solve?

Option<T> represents a value that may be present or absent. It forces Rust code to acknowledge both conditions, reducing runtime failures caused by unexpectedly missing or null values.

Does Rust have no null values at all?

Safe Rust references cannot be null, and optional values normally use Option<T>. Null raw pointers can still appear in unsafe Rust and foreign-function interfaces when interacting with lower-level systems.

What does cargo doc --open do?

It generates HTML documentation for the current Rust package and its dependencies, stores that documentation under the project’s target directory, and opens it using the configured or default browser.

Can Rust applications run without Rust being installed?

Yes. A compiled Rust executable does not need the Rust compiler or Cargo on the target machine. However, it must match the target operating system, architecture, ABI, and any required system libraries.

Is Rust a replacement for JavaScript or Ruby?

No. Rust is valuable when control, safety, and performance are primary constraints. JavaScript and Ruby often remain better choices for web applications that prioritize ecosystem reach and rapid product development.

Our Final Words

My Raspberry Pi experiment did not convince me to rewrite every JavaScript and Ruby application in Rust. It did something more useful: it showed me how differently a language can approach documentation, mutation, missing values, data transformation, and deployment.

cargo doc --open removes the hunt for version-specific documentation. Default immutability makes state changes deliberate. Shadowing creates a clean distinction between mutation and transformation. Option<T> moves missing-value handling into the type system. Native compilation produces deployment artifacts that do not require a language runtime.

These benefits come with stricter rules, longer compile cycles, cross-compilation considerations, and a learning curve that should not be dismissed.

JavaScript and Ruby are still the languages I would reach for in many web projects. But after working with Rust, I find myself writing both more deliberately—using fewer mutable variables, modeling absent data more carefully, and paying closer attention to what production environments actually need.

Author-Devesh Mhatre
Devesh Mhatre

Tech enthusiast with a passion for open-source software and problem-solving. Experienced in web development, with a focus on React, React Native and Rails. I use arch (and neovim) btw ;)

Share this article

Phone

Next for you

8 Best GraphQL Libraries for Node.js in 2025 Cover

Technology

Aug 4, 202613 min read

8 Best GraphQL Libraries for Node.js in 2025

8 Best GraphQL Libraries for Node.js in 2026 Too Long? Read This First - Choose Apollo Server when you need a mature ecosystem, GraphOS integration, plugins, or Apollo Federation. - Choose GraphQL Yoga for a modern, portable server with Fetch API compatibility and built-in support for subscriptions over Server-Sent Events. - Choose Mercurius when your application already uses Fastify and runtime efficiency is a major priority. - Use GraphQL.js when you need the official JavaScript implementati

9 React Native Animation Libraries and Tools Compared Cover

Technology

Aug 4, 202615 min read

9 React Native Animation Libraries and Tools Compared

Too Long? Read This First - Use React Native Reanimated for gesture-driven, interruptible, and performance-sensitive interface animations. - Use the built-in Animated API for simple fades, transforms, and timed sequences without another dependency. - Pair React Native Gesture Handler with Reanimated for swipes, dragging, pinching, rotation, and other touch-driven experiences. - Use Lottie React Native for non-interactive motion graphics supplied by designers. - Choose React Native Skia for cust

9 Critical Practices for Secure Web Application Development Cover

Technology

Aug 4, 202616 min read

9 Critical Practices for Secure Web Application Development

Too Long? Read This First - Define security requirements and model threats before implementation begins. - Treat authentication, account recovery, and MFA as one complete identity system. - Apply server-side authorization to every protected action and object. - Prevent injection with parameterized APIs, structured validation, safe output handling, and restricted outbound requests. - Protect sessions and tokens throughout their complete lifecycle. - Minimise sensitive data and manage encryption