5 Rust Features I Wish JavaScript and Ruby Had

-
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 --openThe 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 --opencreates 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 --openTo include private functions, structs, and modules from the current package, use:
cargo doc --document-private-items --openFor locally installed Rust standard-library documentation, you can run:
rustup docHow 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 --openThere 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" }.freezeHowever, 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:
user_inputinitially holds a string containing whitespace.- The next
user_inputholds a trimmed string slice. - The final
user_inputholds 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
mutwhen 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_iBut 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:
nullundefined- 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&.cityThese 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 --releaseCargo 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_applicationHow 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_modulesdirectory
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-gnuYou would then build for that target:
cargo build \
--release \
--target aarch64-unknown-linux-gnuCross-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
| Capability | Rust | JavaScript | Ruby |
| Default variable behavior | Immutable | Mutable with let; non-reassignable binding with const | Mutable |
| Same-scope shadowing | Supported with let | Not supported with let or const | Reassignment rather than typed shadowing |
| Missing values | Option<T> in safe code | null, undefined, optional properties | nil |
| Compile-time null handling | Enforced through types | Available with TypeScript configuration | Available through optional tooling such as Sorbet |
| Local dependency documentation | Integrated through Cargo | Depends on package and documentation tooling | Available through RI/RDoc, but less unified |
| Production runtime | Native executable | Requires a JavaScript runtime | Requires a Ruby interpreter |
| Type checking | Static and compile-time | Dynamic; static with TypeScript | Dynamic; optional static tooling |
| Primary strength | Safety, control, and predictable performance | Web ecosystem and development flexibility | Expressive application development |
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
constunless 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.



