As a frontend developer, I’ve spent most of my time working with JavaScript and TypeScript—where variables are flexible, memory management is automatic, and “undefined” is just part of life. But when I started exploring Rust, I quickly realised how different the concept of a variable can be when memory safety is a core design principle rather than an afterthought.
In JavaScript, declaring a variable with let or const feels simple: you store a value, reuse it, and rely on the garbage collector to clean things up later. Rust, on the other hand, challenges this comfort zone. Every variable in Rust is tied to a set of strict yet elegant rules — about ownership, borrowing, and lifetimes — that ensure memory is handled safely and predictably without a garbage collector.
At first, Rust’s compiler felt overly strict. It refused to let me do things JavaScript would easily allow. But the more I coded, the more I realised these “restrictions” are actually guarantees — preventing entire classes of runtime errors before the program even runs. In this article, I’ll share what I learned while transitioning from JavaScript’s flexible variable system to Rust’s ownership-driven model, and how it reshaped the way I think about data, memory, and safety in programming.
Types of Memory in Rust
*Before understanding how ownership works, it’s important to know *where Rust stores data. Like most systems programming languages, Rust primarily uses two types of memory: the **stack and the heap. Each has different characteristics and plays a key role in how Rust manages data safely without a garbage collector.
1. Stack Memory
The stack is a region of memory that stores data in a Last-In, First-Out (LIFO) manner.
It’s fast, predictable, and automatically cleaned up when a variable goes out of scope.
- Data stored on the stack must have a fixed size known at compile time.
- Primitive types like integers, floats, and booleans are stored here.
-
Function calls and local variables are also managed in the stack.
fn add(a: i32, b: i32) -> i32 { let sum = a + b; // stored in stack frame of `add` sum } fn main() { let x = 10; // stored in stack frame of `main` let y = 20; // stored in stack frame of `main` let result = add(x, y); // `add` frame pushed onto stack println!("{}", result); }
2. Heap Memory
The heap is where data of unknown or dynamic size lives.
It’s more flexible but slightly slower to access compared to the stack.
- Used for values whose size can change or isn’t known at compile time.
- Data is allocated using pointers, and ownership determines who’s responsible for freeing it.
- Common examples include
StringandVec.
fn main() {
let s = String::from("Rust");
}
Ownership Rules:
Rust's ownership system is built on three fundamental rules that the compiler enforces at compile time:
The Three Rules
- Each value in Rust has a single owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
Borrowing & References:
What Is Ownership?
Ownership defines who is responsible for a piece of data and when that data will be freed from memory.
fn main() {
let s1 = String::from("Rust");
let s2 = s1; // ownership moved
println!("{}", s1); // ❌ ERROR: s1 no longer owns the data
}
📌 Ownership transfers responsibility for the value.
What Is Borrowing?
Borrowing lets you use a value without taking ownership.
This is done through references (&T or &mut T).
fn main() {
let s = String::from("Rust");
print_string(&s); // borrow instead of move
println!("{}", s); // ✔ s is still valid
}
fn print_string(text: &String) {
println!("{}", text);
}
📌 Borrowing allows access but does not transfer responsibility.
Practical Examples:
// *********=> INTEGER, STRING, TUPLE, VECTOR, FUNCTION, OWNERSHIP, BORROWINGA
fn main() {
// *********=> INTEGER (Copy type, lives on stack)
let mut num: u8 = 5;
// let num = 5; // Rust can infer the type for simple numeric values
println!("value stored in num: {}", num);
num = 2; // to make a variable mutable, add `mut` in front of the name
println!("value stored in num: {}", num);
// *********=> STRING (heap-allocated)
// let string_literals: &str = "HELLO WORLD"; // &str = string slice (string literal)
let string = String::from("HELLO WORLD"); // heap-allocated growable string
println!("string: {}", string);
// *********=> TUPLE
let emp_info: (&str, u8) = ("Ramesh", 50);
let (emp_name, emp_age) = emp_info;
println!("Employee name: {}, age: {}", emp_name, emp_age);
// let emp_name = emp_info.0;
// let emp_age = emp_info.1;
// *********=> FUNCTION CALLING (Copy types)
let num1: u8 = 10;
let num2: u8 = 20;
let result: u8 = print_value(num1, num2);
println!("result: {}", result);
// num1 and num2 are still valid here because u8 implements Copy.
// *********=> OWNERSHIP BASICS
ownership_func();
ownership_func2();
// *********=> ARRAYS AND VECTORS
array_fn();
vector_fn();
// *********=> SHADOWING
shadowing();
}
// add parameter type int,string
fn print_value(num1: u8, num2: u8) -> u8 {
num1 + num2
}
// *********=> OWNERSHIP: SCOPE
// global scope
const GLOBAL_CONST: u8 = 5; // const requires type annotation and should be UPPERCASE
fn ownership_func() {
println!("GLOBAL_CONST: {}", GLOBAL_CONST);
// function scope
let outside_variable = 5;
// block scope
{
let inside_variable = 5;
println!("INSIDE VARIABLE: {}", inside_variable);
}
// inside_variable is not accessible here
println!("OUTSIDE VARIABLE: {}", outside_variable);
}
fn ownership_func2() {
// This works because integers are Copy types (stack-allocated, small, trivial)
let a = 5;
let b = a; // a is copied
println!("a: {}, b: {}", a, b);
// For heap types like String, ownership is moved, not copied by default
let str1: String = String::from("HELLO"); // str1 owns "HELLO"
let str2 = str1; // ownership moved from str1 to str2
// println!("{}", str1); // ❌ error: str1 is no longer valid here
println!("str2: {}", str2);
let x: String = String::from("HELLO BYE");
// transfer ownership of x into function new_ownership_func
new_ownership_func(x);
// println!("{}", x); // ❌ error: x was moved
}
fn new_ownership_func(item: String) {
println!("item (taken by ownership): {}", item);
let mut s: String = String::from("beyeyyeee"); // s owns this String
// Borrowing mutable reference to s
let len: usize = new_ownership_func2(&mut s); // passing &mut s (borrow, not move)
println!("VALUE of s: '{}', len before push: {}", s, len);
// *********=> REFERENCE RULES
let mut s1: String = String::from("Hello");
{
// First mutable borrow in this inner scope
let w1 = &mut s1;
w1.push_str(" World");
println!("w1 = {}", w1);
} // w1 goes out of scope here, so we can borrow mutably again
{
// Second mutable borrow in a separate scope
let w2 = &mut s1;
w2.push_str(" Again");
println!("w2 = {}", w2);
} // w2 goes out of scope
println!("Final s1: {}", s1);
// You cannot have two active &mut references at the same time.
// This would be an error:
//
// let r1 = &mut s1;
// let r2 = &mut s1; // ❌ cannot borrow `s1` as mutable more than once at a time
}
fn new_ownership_func2(item: &mut String) -> usize {
let length = item.len();
item.push_str("dsvndfvdv"); // modify through mutable reference
new_main();
length
}
fn new_main() {
// *********=> REFERENCE AND DEREFERENCE
let mut x = 5;
x = x + 1;
let y = &mut x; // y is a mutable reference to x
*y = *y + 1; // use * to dereference and modify the underlying value
println!("y (mutable ref to x): {}", y);
}
fn array_fn() {
// *********=> ARRAY
// fixed-size, same-type, lives on the stack
let mut arr: [&str; 3] = ["HELLO", "BYE", "CODER"];
// Here arr is copied into arr1 (arrays of Copy types are Copy up to a certain size)
write_mut_fn(arr);
// Here we pass a mutable reference to the original arr
write_ref_fn(&mut arr);
println!("arr in array_fn (after write_ref_fn): {:?}", arr);
}
fn write_mut_fn(mut arr1: [&str; 3]) {
// arr1 is a copy of arr and is local to this function
arr1[0] = "FELLOW";
println!("arr1 in write_mut_fn: {:?}", arr1);
}
fn write_ref_fn(arr2: &mut [&str; 3]) {
// We are modifying the original arr via mutable reference
arr2[0] = "FELLOW2";
println!("arr2 in write_ref_fn: {:?}", arr2);
}
fn vector_fn() {
// *********=> VECTOR (heap-allocated growable array)
let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];
// Pass a mutable reference to the vector (no clone, no move of ownership)
write_vector_mut_fn(&mut v);
println!("v after write_vector_mut_fn: {:?}", v);
}
fn write_vector_mut_fn(v: &mut Vec<i32>) {
v.push(99);
println!("v inside write_vector_mut_fn: {:?}", v);
}
fn shadowing() {
// *********=> SHADOWING
let x = 5;
println!("value of x: {}", x);
let x = 10; // shadows previous x
println!("value of x after shadowing: {}", x);
}

Top comments (0)