JavaScript / ES6+ Learning – Lesson 6 – this, call, apply, bind, Arrow Functions and Closures

This is one of the most important JavaScript lessons for us as a Ruby/Rails developer.

After this lesson, we should be able to read code like:

const user = {
name: "John",
greet() {
console.log(this.name);
}
};

and understand exactly what this means.

We’ll also understand why:

const greet = user.greet;

can behave differently from:

user.greet();

Then we’ll connect that to:

useEffect(() => {
...
});

and eventually to closures, which are fundamental to React and Node.js.


1. What is this?

The easiest starting point is:

this is a special value available inside a function.

But unlike Ruby’s self, its value is usually determined by how a regular function is called.

Consider:

const user = {
name: "John",
greet() {
console.log(this.name);
}
};
user.greet();

Output:

John

Why?

Because we called:

user.greet();

The object before the . is the receiver.

So:

user.greet()
↓
this = user

Therefore:

this.name

is effectively:

user.name

2. Think about the call site

This is the most useful mental model:

user.greet();

Look at the left side of the dot:

user . greet()
^^^^
receiver

For a normal method call, that receiver becomes this.

Example:

const person = {
name: "Alice",
sayName() {
console.log(this.name);
}
};
person.sayName();

Here:

this → person

Output:

Alice

3. But this is not permanently attached to the function

This is where JavaScript differs from the simple mental model we may have from Ruby.

Take:

const user = {
name: "John",
greet() {
console.log(this.name);
}
};

Now:

user.greet();

gives:

John

But:

const greet = user.greet;
greet();

is a different call.

We did:

const greet = user.greet;

So greet now refers to the function.

But we’re no longer calling it as:

user.greet()

We’re calling:

greet()

There is no receiver.

So the this behavior is different.


4. The three things we should distinguish

When we see a function, distinguish:

user.greet
user.greet()
const fn = user.greet;
fn();

They mean different things.

user.greet

Get the function.

user.greet()

Call the function with user as the method receiver.

fn()

Call the function independently.

This distinction is extremely important in JavaScript.


5. this in a regular function

Consider:

function showThis() {
console.log(this);
}

What this is depends on how we invoke the function.

That’s the core idea:

regular function
↓
how was it called?
↓
determines `this`

There are several invocation patterns, and we’ll learn the important ones.


6. Method call

The easiest:

const user = {
name: "John",
greet() {
console.log(this.name);
}
};
user.greet();

Here:

this = user

7. Explicitly set this

JavaScript gives us:

call()
apply()
bind()

These let us control what this refers to.

This is a very common int. topic.


8. call

Suppose:

function greet() {
console.log(this.name);
}

We have an object:

const user = {
name: "John"
};

We can explicitly say:

greet.call(user);

Now:

this = user

Therefore:

John

So:

greet.call(user);

means approximately:

Call greet now, and make this equal to user.


9. call with arguments

Suppose:

function greet(message) {
console.log(`${message}, ${this.name}`);
}

Then:

const user = {
name: "John"
};

Call:

greet.call(user, "Hello");

Output:

Hello, John

The structure is:

function.call(thisArg, arg1, arg2, ...)

For example:

greet.call(user, "Hello");

means:

this = user
message = "Hello"

10. apply

apply is similar to call.

greet.apply(user, ["Hello"]);

Output:

Hello, John

The difference is mainly how arguments are supplied.

call

greet.call(user, "Hello", "How are you?");

apply

greet.apply(user, ["Hello", "How are you?"]);

So:

call → arguments individually
apply → arguments as an array

Historically apply was particularly useful when we already had an array of arguments. With modern JavaScript, spread syntax has reduced some of those use cases.


11. bind

bind is different.

const boundGreet = greet.bind(user);

This does not immediately call greet.

Instead it creates a new function.

Think:

greet.bind(user)
↓
new function
↓
this permanently set to user

Then:

boundGreet("Hello");

Output:

Hello, John

So:

call → call now
apply → call now
bind → create another function

That’s the key distinction.


12. call vs apply vs bind

Memorize this table:

MethodExecutes immediately?Arguments
callYesIndividual
applyYesArray
bindNoReturns new function

Example:

fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);
const newFn = fn.bind(obj);
newFn(1, 2);

13. A practical example

Suppose:

const user1 = {
name: "John"
};
const user2 = {
name: "Jane"
};
function greet(message) {
console.log(`${message}, ${this.name}`);
}

Now:

greet.call(user1, "Hello");

Output:

Hello, John

And:

greet.call(user2, "Hello");

Output:

Hello, Jane

Same function.

Different this.

This demonstrates why this in regular functions is associated with the invocation.


14. Arrow functions change the rules

Now comes a critical concept.

Arrow functions do not get their own this.

They use this from the surrounding lexical scope.

Example:

const user = {
name: "John",
greet: () => {
console.log(this.name);
}
};

We might think:

user.greet()
↓
this = user

But that’s not how an arrow function works.

An arrow function does not create its own this.

It captures this from the surrounding scope.


15. Why this matters

Compare:

Regular function

const user = {
name: "John",
greet() {
console.log(this.name);
}
};
user.greet();

Here:

this = user

Arrow function

const user = {
name: "John",
greet: () => {
console.log(this.name);
}
};

Here this does not become user.

This is why object methods generally should not be written as arrow functions when they need dynamic this.


16. The classic callback problem

Now we get to a very common JavaScript issue.

Suppose:

const user = {
name: "John",
greet() {
setTimeout(function() {
console.log(this.name);
}, 1000);
}
};
user.greet();

We might expect:

John

But the inner regular function has its own this behavior.

The setTimeout callback isn’t being called as:

user.someMethod()

So it doesn’t automatically inherit the outer method’s this.


17. Arrow functions solve this nicely

Now:

const user = {
name: "John",
greet() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
user.greet();

Now the arrow function:

() => {
console.log(this.name);
}

doesn’t create a new this.

It captures the this from greet().

So:

user.greet()
↓
this = user
↓
arrow callback
↓
uses surrounding this
↓
this = user

Output:

John

This is one of the biggest reasons arrow functions are so useful.


18. Arrow functions and React

We’ll often see:

<button onClick={() => handleClick()}>
Click
</button>

Here the arrow function is primarily being used as a callback.

React will call the arrow function later when the event occurs.

Similarly:

useEffect(() => {
loadUsers();
}, []);

The callback passed to useEffect is an arrow function.

Again, remember:

() => {
...
}

is just a function.

React receives that function and decides when to invoke it.


19. Now: closures

This is perhaps even more important than this.

A closure happens when a function remembers variables from its surrounding lexical scope.

Consider:

function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}

Call:

const counter = createCounter();

Now:

counter(); // 1
counter(); // 2
counter(); // 3

The amazing part is:

createCounter() has already finished executing.

Yet the returned function can still access:

count

Why?

Because the function closed over the variable.


20. Visualize the closure

When we execute:

const counter = createCounter();

think:

createCounter()
|
| creates
v
count = 0
|
| returns function
v
counter ────────→ function
|
└── remembers count

Then:

counter();

the function can still access the remembered count.

After:

counter();

the value becomes:

count = 1

Next:

counter();

the function still has access to the same variable:

count = 2

21. Closure does NOT mean copying the value

This is an important detail.

Some beginners imagine:

count = 0

gets copied into the function.

That’s not the best mental model.

Instead, the function retains access to the lexical environment containing count.

Therefore:

counter();
counter();
counter();

all operate on the same captured variable.


22. Why closures are useful

Closures allow us to keep private state.

Example:

function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
},
getBalance() {
return balance;
}
};
}

Now:

const account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance());

Result:

1500

But:

account.balance

doesn’t exist as a public property.

The state is captured by the closure.

This is conceptually similar to encapsulation.


23. Closures and callbacks

Callbacks frequently create closures.

Example:

Output:

Hello John

The returned callback remembers:

name = "John"

even after greetUser() finished.

So:

outer function
↓
creates variable
↓
creates callback
↓
callback remembers variable

That’s a closure.


24. Closures in loops

Here’s a classic int. example:

for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}

Many people expect:

0
1
2

but with var, all callbacks refer to the same function-scoped i, which has reached 3 when the callbacks run.

So we get:

3
3
3

This is a classic closure + asynchronous execution int. question.

Now change var to let:

for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}

Now the callbacks see the per-iteration i values:

0
1
2

This connects directly to our Lesson 2 discussion of let and block/iteration scoping.


25. Closure + React

Closures are everywhere in React.

For example:

function User({ user }) {
const handleClick = () => {
console.log(user.name);
};
return (
<button onClick={handleClick}>
Click
</button>
);
}

The function:

handleClick

uses:

user

from its surrounding scope.

So it forms a closure over user.

This is normal JavaScript behavior, not React-specific magic.


26. Closure + useEffect

Consider:

function UserComponent({ userId }) {
useEffect(() => {
console.log(userId);
}, [userId]);
return null;
}

The callback passed to useEffect accesses:

userId

from the surrounding function scope.

That’s a closure.

This is why closures and React hooks are closely related.

Later, when we study the dependency array deeply, this will become very important.


27. Closure + Node.js

Closures are equally useful in Node.

For example:

function createLogger(prefix) {
return message => {
console.log(`[${prefix}] ${message}`);
};
}
const errorLogger = createLogger("ERROR");
const infoLogger = createLogger("INFO");
errorLogger("Database failed");
infoLogger("Server started");

Output:

[ERROR] Database failed
[INFO] Server started

Each returned function remembers its own prefix.

So closures are not merely “React stuff.”

They’re a fundamental JavaScript feature.


28. this vs closure

These are different concepts.

this

Answers:

What object/context does this regular function call use as this?

Closure

Answers:

What variables from surrounding lexical scopes can this function still access?

For example:

const user = {
name: "John",
greet() {
const message = "Hello";
setTimeout(() => {
console.log(this.name);
console.log(message);
}, 1000);
}
};

The arrow callback has access to:

this
message

but for different reasons:

this → inherited from surrounding context
message → closure over surrounding lexical variable

This distinction is worth remembering.


29. Ruby comparison

Ruby has:

self

JavaScript has:

this

But don’t assume:

Ruby self == JavaScript this

They have important differences.

Ruby self is much more naturally tied to the current execution context/object.

JavaScript this for a regular function is heavily dependent on how the function is called.

And JavaScript arrow functions don’t create their own this.

That distinction is especially important when moving from Rails backend code to React/Node.


30. A very useful mental model

For regular functions, ask:

How was this function called?

For arrow functions, ask:

What is the surrounding `this`?

For closures, ask:

Which variables from outer scopes does this function use?

These three questions will solve a huge number of JavaScript puzzles.


🎯 Interview questions

Try these without running them.

Q1

What is this here?

const user = {
name: "John",
greet() {
console.log(this.name);
}
};
user.greet();

Q2

What’s the problem here?

const greet = user.greet;
greet();

Q3

What’s the difference between:

greet.call(user);

and:

const fn = greet.bind(user);

Q4

What is the difference between call and apply?

Q5

What is a closure?

Q6

Why does this work?

function createCounter() {
let count = 0;
return () => ++count;
}
const counter = createCounter();
counter(); // ?
counter(); // ?

Q7

Why does var produce the classic loop problem?

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}

🧪 Live coding practice

Problem 1 – this

Predict the output:

const user = {
name: "John",
greet() {
return `Hello ${this.name}`;
}
};
console.log(user.greet());

Problem 2 – call

Write:

function introduce(role) {
console.log(`${this.name} is a ${role}`);
}

Then use call so that:

const user = {
name: "Abhilash"
};

produces:

Abhilash is a Senior Software Engineer

Problem 3 – bind

Given:

const user = {
name: "John"
};
function greet(message) {
console.log(`${message}, ${this.name}`);
}

Create:

const boundGreet = ...

so that:

boundGreet("Hello");

produces:

Hello, John

Problem 4 – Closure

Implement:

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

The count variable should not be directly accessible from outside.


Problem 5 – Closure with argument

Create:

const double = multiplier(2);
double(5); // 10
double(10); // 20

The function multiplier(2) should return another function.

This is an excellent exercise for understanding closures.


Problem 6 – Callback + closure

Implement:

function createGreeter(name) {
return function() {
console.log(`Hello ${name}`);
};
}

Then:

const greetJohn = createGreeter("John");
const greetJane = createGreeter("Jane");
greetJohn();
greetJane();

Expected:

Hello John
Hello Jane

Be able to explain why each returned function remembers a different name.


Final mental model

We now have four layers:

FUNCTION
↓
Function can be passed around as a value
CALLBACK
↓
A function passed to another function
THIS
↓
For regular functions, determined by how the function is called
CLOSURE
↓
A function retains access to variables from its lexical scope

And the most important distinctions:

user.greet
↓
function reference
user.greet()
↓
function call
↓
this = user
greet.call(user)
↓
call now with explicit this
greet.apply(user, args)
↓
call now with explicit this + array arguments
greet.bind(user)
↓
return a new function with bound this

For arrow functions:

arrow function
↓
doesn't create its own `this`
↓
captures surrounding `this`

For closures:

outer scope
↓
function created inside it
↓
inner function remembers outer variables

The connection to React

We can now look at:

function User({ user }) {
const handleClick = () => {
console.log(user.name);
};
return (
<button onClick={handleClick}>
Click
</button>
);
}

and identify:

{ user } → object destructuring
handleClick → function
() => → arrow function
onClick={handleClick}→ function reference
user.name → closure over user

That’s the exact JavaScript foundation we want before going further into React.

Next lesson: JavaScript asynchronous programming – synchronous execution, call stack, event loop, Web/Node APIs, callback queue, microtask queue, Promises, and why async/await works. This will be especially important for us because it connects JavaScript directly to Node.js.

Lesson 7: https://railsdrop.com/2024/12/10/javascript-es6-learning-lesson-7-asynchronous-javascript-event-loop-promises-and-async-await/

JavaScript / ES6+ Learning – Lesson 5 – JavaScript Objects, Destructuring, Spread and this

This lesson connects several things you’ve already seen:

const [users, setUsers] = useState([]);

and:

const { name, age } = user;

and React props such as:

<User name="John" age={30} />

The goal is to understand how JavaScript objects work and how we extract, copy, and pass their data around.


1. JavaScript objects

An object is a collection of properties:

const user = {
id: 1,
name: "John",
age: 30,
active: true
};

Think of it as:

user
|
+-- id → 1
+-- name → "John"
+-- age → 30
+-- active → true

Access properties:

user.name; // "John"
user.age; // 30
user.active; // true

You can also use bracket notation:

user["name"];

These are equivalent when the property name is known.


2. Dot notation vs bracket notation

This works:

const user = {
name: "John"
};
console.log(user.name);

But bracket notation becomes particularly useful when the property name is stored in a variable:

const property = "name";
console.log(user[property]);

Result:

John

This would not work the same way:

user.property;

because that looks for a property literally called "property".


3. Objects can contain functions

For example:

const user = {
name: "John",
greet() {
console.log("Hello");
}
};

Call it:

user.greet();

The function is a property of the object.

When a function belongs to an object like this, we commonly call it a method.


4. Objects can contain anything

An object can contain:

const user = {
id: 1,
name: "John",
active: true,
roles: ["admin", "editor"],
address: {
city: "Kochi",
country: "India"
},
greet() {
console.log("Hello");
}
};

So you can have:

object
├── primitive
├── array
├── object
└── function

This is why JavaScript objects are extremely flexible.


5. Object destructuring

Now the first very important ES6 feature.

Suppose:

const user = {
id: 1,
name: "John",
age: 30
};

You could write:

const name = user.name;
const age = user.age;

But destructuring allows:

const { name, age } = user;

This means:

const name = user.name;
const age = user.age;

Conceptually, yes.

So:

const { name, age } = user;

means:

Take the `name` property from user
↓
put it into variable `name`
Take the `age` property from user
↓
put it into variable `age`

6. Array destructuring vs object destructuring

This is one of the most important distinctions from the previous lessons.

Array

const numbers = [10, 20];
const [a, b] = numbers;

Destructuring is based on position:

numbers[0] → a
numbers[1] → b

Object

const user = {
name: "John",
age: 30
};
const { name, age } = user;

Destructuring is based on property name:

user.name → name
user.age → age

So:

Array destructuring → position
Object destructuring → property name

This distinction should become automatic.


7. Rename while destructuring

Suppose:

const user = {
name: "John",
age: 30
};

You don’t have to name your variable name.

You can do:

const { name: userName, age: userAge } = user;

Now:

userName; // "John"
userAge; // 30

Read this as:

name property → variable userName
age property → variable userAge

This syntax looks strange initially, but you’ll see it in real React and Node code.


8. Default values in destructuring

Suppose:

const user = {
name: "John"
};

You can write:

const { name, age = 30 } = user;

Now:

name; // "John"
age; // 30

The default is used only when the property is undefined.


9. Nested destructuring

Given:

const user = {
name: "John",
address: {
city: "Kochi",
country: "India"
}
};

You can do:

const {
name,
address: { city, country }
} = user;

Now:

name; // "John"
city; // "Kochi"
country; // "India"

This is useful, but don’t overuse deep destructuring. Very complex destructuring can become harder to read than ordinary property access.


10. Function parameters can be destructured

This is very common in React.

Instead of:

function User(props) {
return <h2>{props.name}</h2>;
}

you can write:

function User({ name }) {
return <h2>{name}</h2>;
}

The second form says:

The function receives an object, and I want its name property.

Suppose React effectively gives:

{
name: "John",
age: 30
}

Then:

function User({ name }) {

extracts:

name = "John";

This is one of the reasons you see React code full of { ... } around function parameters.


11. useState vs props: two different destructuring styles

Now compare these:

const [users, setUsers] = useState([]);

and:

function User({ name, age }) {
}

The first is array destructuring:

position 0 → users
position 1 → setUsers

The second is object destructuring:

name property → name
age property  → age

This is a very important React distinction.


12. Spread syntax with objects

Let’s revisit spread:

const user = {
name: "John",
age: 30
};
const copy = {
...user
};

Now:

copy

contains:

{
name: "John",
age: 30
}

The spread operator copies the object’s own enumerable properties into a new object.

At a beginner level, think:

“Take the properties from this object and put them into a new object.”


13. Updating an object using spread

Suppose:

const user = {
name: "John",
age: 30
};

We want:

same user
but age = 31

Write:

const updatedUser = {
...user,
age: 31
};

Result:

{
name: "John",
age: 31
}

Why does age: 31 win?

Object properties appearing later overwrite earlier properties with the same key.

So conceptually:

...user
↓
name: John
age: 30
age: 31
↓
overwrite age

14. This is common in React state updates

Suppose:

const [user, setUser] = useState({
name: "John",
age: 30
});

To change just the age:

setUser({
...user,
age: 31
});

We don’t do:

user.age = 31;

for normal React state updates.

Instead, we create a new object.

Conceptually:

old object
↓
{ name: "John", age: 30 }
...copy...
new object
↓
{ name: "John", age: 31 }

This gives React a new object reference.

We’ll later connect this directly to the question you raised previously about why React cares about new references.


15. Spread with arrays

Given:

const users = ["John", "Jane"];

Append without modifying the original:

const newUsers = [...users, "Mike"];

Result:

["John", "Jane", "Mike"]

Prepend:

const newUsers = ["Mike", ...users];

Result:

["Mike", "John", "Jane"]

Merge:

const first = [1, 2];
const second = [3, 4];
const combined = [...first, ...second];

Result:

[1, 2, 3, 4]

16. Spread is shallow

This is an important interview topic.

Consider:

const user = {
name: "John",
address: {
city: "Kochi"
}
};
const copy = {
...user
};

copy is a new object.

But:

copy.address === user.address

is:

true

Why?

Because spread creates a shallow copy.

The outer object is new, but nested objects are still shared.

Visualize:

user ────────────────┐
│
copy ────────────────┤
↓
address
{ city: "Kochi" }

Both objects point to the same nested address object.

This becomes extremely important when we talk about immutable state.


17. Deep copy is a different problem

Don’t assume:

const copy = { ...user };

means:

“Everything inside user has been recursively copied.”

It hasn’t.

For modern applications there are several strategies for deep cloning, but don’t jump to one automatically. Often you don’t actually need a deep clone.

For React state, it’s usually better to create new references only along the part of the structure you’re changing.

For example:

const updatedUser = {
...user,
address: {
...user.address,
city: "Bengaluru"
}
};

Now both the outer object and changed nested object are new references.


18. Object shorthand

ES6 gives us convenient syntax here:

const name = "John";
const age = 30;
const user = {
name,
age
};

Instead of:

const user = {
name: name,
age: age
};

JavaScript understands that:

name

means:

name: name

when used in an object literal.

You’ll see this everywhere.


19. Computed property names

You can dynamically create a property name:

const field = "name";
const user = {
[field]: "John"
};

Result:

{
name: "John"
}

Another example:

const field = "email";
const value = "john@example.com";
const user = {
[field]: value
};

Result:

{
email: "john@example.com"
}

This is useful when dynamically building objects.


20. this – the important part

Now we reach one of JavaScript’s most confusing concepts for Ruby developers:

this

Consider:

const user = {
name: "John",
greet() {
console.log(this.name);
}
};
user.greet();

Output:

John

Here:

this

refers to the object used to call the method:

user.greet();

So conceptually:

user.greet()
↓
this = user

Therefore:

this.name

is:

user.name

21. A critical difference from Ruby

In Ruby, you may mentally think:

“self is the current object.”

JavaScript’s this is more nuanced.

One of the most useful rules:

For a normal function call, this is determined by how the function is called, not simply where the function was defined.

That means:

user.greet();

and:

const greet = user.greet;
greet();

can have different this behavior.

This is one of the reasons JavaScript’s this can be tricky.


22. Arrow functions and this

Arrow functions behave differently.

They do not create their own this.

Consider:

const user = {
name: "John",
greet: () => {
console.log(this.name);
}
};
user.greet();

Do not assume:

this = user

just because the function appears inside the object.

That is not how arrow functions work.

This is one of the reasons you should not blindly replace every regular function with an arrow function.

We’ll spend an entire lesson on this, call, apply, bind, and arrow-function behavior.


23. Object references

Now an important JavaScript behavior.

const user1 = {
name: "John"
};
const user2 = user1;

We did not create a new object.

Both variables refer to the same object:

user1 ─────┐
↓
{ name: "John" }
↑
user2 ─────┘

Therefore:

user2.name = "Jane";
console.log(user1.name);

prints:

Jane

This is because objects are reference values.


24. Compare primitive values

Now compare:

let a = 10;
let b = a;
b = 20;
console.log(a);

Result:

10

For the primitive number:

a → 10
b → 10
then:
b → 20

Changing b doesn’t alter a.

With objects:

user1 ──┐
↓
object
↑
user2 ──┘

Both references point to the same object.

This distinction will become crucial when we discuss:

  • React state
  • equality
  • immutability
  • shallow comparison
  • useMemo
  • useCallback

25. === with objects

This gives us another important interview question.

const user1 = { name: "John" };
const user2 = { name: "John" };
console.log(user1 === user2);

Result:

false

Why?

Because these are two different objects.

Even though their contents are identical:

user1 → Object A
user2 → Object B

They have different references.

But:

const user1 = { name: "John" };
const user2 = user1;
user1 === user2;

returns:

true

because both reference the same object.


26. This explains something important in React

Later you’ll see code like:

setUsers(users);

versus:

setUsers([...users]);

The first gives React the same array reference.

The second creates a new array:

users ───────→ old array
[...users] ──→ new array

This distinction becomes important because React can use reference identity when determining whether state/props values changed.

We’ll go deep into this when we revisit React state and rendering.


27. Complete React example

Let’s put today’s concepts together.

function User({ user }) {
const { name, age } = user;
return (
<div>
<h2>{name}</h2>
<p>{age}</p>
</div>
);
}

Here we have:

Object parameter

{ user }

Object destructuring

const { name, age } = user;

Function

function User(...) {
}

And React passes an object as the argument.


28. Another React example

Suppose:

const [user, setUser] = useState({
name: "John",
age: 30,
city: "Kochi"
});

Update only the city:

setUser({
...user,
city: "Bengaluru"
});

This combines:

object
+
spread
+
new object reference
+
state update

And now you can see why learning JavaScript first is useful.


🧠 The five concepts to remember

At the end of this lesson, these should be clear:

1. Object destructuring

const { name, age } = user;

Means:

const name = user.name;
const age = user.age;

2. Array destructuring

const [first, second] = numbers;

Means:

const first = numbers[0];
const second = numbers[1];

3. Spread

const copy = { ...user };

Creates a new shallow object.

4. References

const b = a;

with objects means both variables refer to the same object.

5. this

For a normal method call:

user.greet();

this inside the method refers to the receiver of that call – user.

Arrow functions have different this behavior.


🎯 Int. questions

Try answering these before reading further.

Q1

What’s the difference?

const [a, b] = [10, 20];

and:

const { a, b } = { a: 10, b: 20 };

Q2

What does this produce?

const user = {
name: "John",
age: 30
};
const { name: userName } = user;

Q3

What is the difference between:

const user2 = user1;

and:

const user2 = { ...user1 };

Q4

What happens here?

const user1 = { name: "John" };
const user2 = { name: "John" };
console.log(user1 === user2);

Q5

What does this do?

const user = {
name: "John",
age: 30
};
const updated = {
...user,
age: 31
};

Q6

Why can this be dangerous to assume?

const copy = { ...user };

when user contains nested objects?


🧪 Live coding practice

Problem 1 – Destructure

Given:

const user = {
id: 10,
name: "John",
age: 30
};

Extract:

id
name
age

using object destructuring.


Problem 2 – Rename

Extract name into a variable called:

userName

Problem 3 – Update immutably

Given:

const user = {
name: "John",
age: 30
};

Create a new object where:

name = "Jane"

without modifying user.


Problem 4 – Add a property

Given:

const user = {
name: "John"
};

Create:

{
name: "John",
active: true
}

using spread.


Problem 5 – Nested object

Given:

const user = {
name: "John",
address: {
city: "Kochi",
country: "India"
}
};

Create a new object where only:

city = "Bengaluru"

changes.

Don’t mutate the original nested address.


Problem 6 – Reference test

Predict:

const a = { count: 1 };
const b = a;
b.count = 2;
console.log(a.count);

Then compare:

const a = { count: 1 };
const b = { ...a };
b.count = 2;
console.log(a.count);

Explain why the outputs differ.


🔗 How this connects to your React learning

You now have almost all the JavaScript pieces needed to understand this:

const [users, setUsers] = useState([]);

and:

function User({ name, age }) {

and:

setUser({
...user,
age: 31
});

They are no longer “React syntax”. They are combinations of ordinary JavaScript:

useState(...) → function call
[users, setUsers] → array destructuring
{ name, age } → object destructuring
...user → spread
user => ... → function / callback

That’s exactly the mental model we want.

Next lesson

Lesson 6 – this, call, apply, bind, arrow functions, and closures.

This is the first lesson where JavaScript will behave noticeably differently from Ruby, and it is especially important for understanding React event handlers and Node.js code.

Lesson 6: https://railsdrop.com/2024/12/09/javascript-es6-bootcamp-lesson-6-this-call-apply-bind-arrow-functions-and-closures/

JavaScript / ES6+ Learning Lesson 4 – Arrays, map, filter, find, reduce, Spread and Rest

This lesson is extremely important for React.

A large amount of React code looks like this:

users.map(...)
users.filter(...)
users.find(...)
items.reduce(...)

Once you understand what these methods are doing with callbacks, React code becomes much easier to read.

The goal today is not to memorize methods. It is to understand the pattern:

array
  ↓
array method
  ↓
callback
  ↓
new result

1. JavaScript arrays

Start with:

const numbers = [10, 20, 30, 40];

An array has:

index: 0 1 2 3
value: 10 20 30 40

Access:

numbers[0]; // 10
numbers[2]; // 30

Length:

numbers.length; // 4

Unlike some languages, JavaScript arrays can contain different types:

const values = [10, "John", true, null];

But in normal application code, arrays usually contain consistent types:

const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];

2. forEach – execute something for every item

Let’s start with the simplest callback method.

const numbers = [1, 2, 3];
numbers.forEach(number => {
console.log(number);
});

Output:

1
2
3

Conceptually:

1 → callback(1)
2 → callback(2)
3 → callback(3)

The important point:

forEach is mainly for performing an action for each item.

For example:

users.forEach(user => {
  console.log(user.name);
});

It does not create a transformed array for you.


3. map – transform every item

This is one of the most important methods for React.

const numbers = [1, 2, 3];

const doubled = numbers.map(number => number * 2);

Result:

[2, 4, 6]

Think:

input
[1, 2, 3]
callback
number => number * 2
output
[2, 4, 6]

The key idea:

map takes every element, runs your callback, and builds a new array from the returned values.


4. What does map actually do?

Conceptually, this:

const doubled = numbers.map(number => number * 2);

behaves roughly like:

const doubled = [];

doubled.push(1 * 2);
doubled.push(2 * 2);
doubled.push(3 * 2);

Result:

[2, 4, 6]

The real implementation is more sophisticated, but this is the right mental model.


5. map with objects

This is where React developers use it constantly.

const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
{ id: 3, name: "Mike" }
];

Extract names:

const names = users.map(user => user.name);

Result:

["John", "Jane", "Mike"]

The callback:

user => user.name

is called once for each object.


6. map can transform objects

Suppose we want to create a different structure:

const users = [
{ id: 1, name: "John", age: 30 },
{ id: 2, name: "Jane", age: 25 }
];
const result = users.map(user => ({
id: user.id,
displayName: user.name
}));

Result:

[
{ id: 1, displayName: "John" },
{ id: 2, displayName: "Jane" }
]

Notice the parentheses:

user => ({
id: user.id,
displayName: user.name
})

We need them because the arrow function is implicitly returning an object.


7. filter – keep matching elements

Now:

const numbers = [1, 2, 3, 4, 5];
const result = numbers.filter(number => number > 2);

Result:

[3, 4, 5]

Unlike map, the callback for filter answers:

Should this item be included?

The callback must produce something truthy or falsy.

Conceptually:

1 → false → remove
2 → false → remove
3 → true  → keep
4 → true  → keep
5 → true  → keep

8. filter with users

const users = [
{ id: 1, name: "John", active: true },
{ id: 2, name: "Jane", active: false },
{ id: 3, name: "Mike", active: true }
];
const activeUsers = users.filter(user => user.active);

Result:

[
{ id: 1, name: "John", active: true },
{ id: 3, name: "Mike", active: true }
]

Very common React pattern:

activeUsers.map(user => (
<div key={user.id}>
{user.name}
</div>
))

This combines two concepts:

filter
↓
choose data
map
↓
transform/render data

9. find – find one item

find looks for the first matching item.

const numbers = [10, 20, 30, 40];
const result = numbers.find(number => number > 20);

Result:

30

Notice something important:

map → returns an array
filter → returns an array
find → returns one item

10. find with objects

const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
const user = users.find(user => user.id === 2);

Result:

{ id: 2, name: "Jane" }

If nothing matches:

const user = users.find(user => user.id === 99);
console.log(user);

Result:

undefined

This is important when handling lookup failures.


11. some

Another useful method:

const numbers = [1, 2, 3, 4];
numbers.some(number => number > 3);

Result:

true

It asks:

Does at least one item satisfy this condition?

For example:

const hasAdmin = users.some(user => user.role === "admin");

12. every

every asks:

Do all items satisfy the condition?

const numbers = [2, 4, 6];
numbers.every(number => number % 2 === 0);

Result:

true

13. reduce – the most important one to understand carefully

reduce often looks scary:

const result = numbers.reduce((total, number) => {
return total + number;
}, 0);

Don’t memorize this.

Understand what it is doing.

Given:

const numbers = [10, 20, 30];

we want:

10 + 20 + 30

reduce maintains an accumulated value.

Think:

initial accumulator
↓
0
0 + 10 = 10
10 + 20 = 30
30 + 30 = 60

Final result:

60

14. Understand the two arguments to reduce

This:

numbers.reduce((total, number) => {
return total + number;
}, 0);

contains:

total
↓
accumulator
number
↓
current element
0
↓
initial value

So:

(total, number) => total + number

means:

Take the accumulated result and the current item, then produce the next accumulated result.


15. reduce step by step

Given:

const numbers = [10, 20, 30];
const total = numbers.reduce(
(sum, number) => sum + number,
0
);

Execution:

First iteration

sum = 0
number = 10
0 + 10 = 10

Accumulator becomes:

10

Second iteration

sum = 10
number = 20
10 + 20 = 30

Third iteration

sum = 30
number = 30
30 + 30 = 60

Final:

60

16. reduce can create objects

This is where reduce becomes powerful.

Suppose:

const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
{ id: 3, name: "Mike" }
];

We want:

{
1: "John",
2: "Jane",
3: "Mike"
}

We can do:

const result = users.reduce((acc, user) => {
acc[user.id] = user.name;
return acc;
}, {});

Result:

{
1: "John",
2: "Jane",
3: "Mike"
}

Don’t worry if reduce still feels less intuitive. That’s normal.

We’ll practice it.


17. map vs filter vs find vs reduce

Keep this table in your head:

MethodPurposeReturns
forEachPerform actionundefined
mapTransform every itemNew array
filterKeep matching itemsNew array
findFind first matchOne item / undefined
someAny match?Boolean
everyAll match?Boolean
reduceAccumulate/build resultAnything

This is much more useful than memorizing code examples.


18. Chain array methods

You can combine methods.

const users = [
{ name: "John", active: true },
{ name: "Jane", active: false },
{ name: "Mike", active: true }
];
const names = users
.filter(user => user.active)
.map(user => user.name);

Step 1:

filter(user => user.active)

produces:

[
{ name: "John", active: true },
{ name: "Mike", active: true }
]

Then:

map(user => user.name)

produces:

["John", "Mike"]

This style is extremely common in React.


19. Spread syntax

Now let’s move to another extremely important ES6 feature.

Suppose:

const numbers = [1, 2, 3];

We can copy its elements into a new array:

const newNumbers = [...numbers];

Result:

[1, 2, 3]

The ... here is called spread syntax.

Think:

...numbers
↓
take elements out
↓
1, 2, 3

20. Add an item with spread

const users = ["John", "Jane"];
const newUsers = [...users, "Mike"];

Result:

["John", "Jane", "Mike"]

This is extremely important in React.

Instead of mutating:

users.push("Mike");

you often create a new array:

setUsers([...users, "Mike"]);

Why?

Because React state updates rely heavily on creating new references rather than mutating existing state.

Check for detial: https://railsdrop.com/react-state-change-causes-react-to-render-reconcile-but-the-important-part-is-how-does-react-know-that-the-state-actually-changed/

21. Spread with objects

Given:

const user = {
name: "John",
age: 30
};

Create a new object:

const updatedUser = {
...user,
age: 31
};

Result:

{
name: "John",
age: 31
}

The later age: 31 overrides the previous value.

Think:

...user
↓
name: John
age: 30
age: 31
↓
overwrite age

22. Spread is heavily used in React

For example:

setUser({
...user,
name: "Jane"
});

Suppose:

user = {
id: 1,
name: "John",
age: 30
};

The result becomes:

{
id: 1,
name: "Jane",
age: 30
}

We didn’t modify the original object.

We created a new object.


23. Rest syntax

Here’s where JavaScript can get confusing.

The same ... syntax can also mean rest.

Compare:

Spread

const numbers = [1, 2, 3];
const copy = [...numbers];

We’re taking values out.

Rest

function sum(...numbers) {
console.log(numbers);
}

Now we’re collecting values into an array.

sum(10, 20, 30);

Inside the function:

numbers = [10, 20, 30]

So:

spread → expand
rest → collect

Same ..., different context.


24. Rest parameters

Example:

function sum(...numbers) {
return numbers.reduce((total, number) => {
return total + number;
}, 0);
}

Now:

sum(10, 20, 30);

Inside:

numbers
// [10, 20, 30]

Result:

60

This is a beautiful example because it combines:

rest
+
reduce
+
callback

25. Destructuring + rest

You can also use rest during destructuring.

const numbers = [10, 20, 30, 40];
const [first, ...rest] = numbers;

Now:

first;
// 10

and:

rest;
// [20, 30, 40]

This means:

first → first item
rest → everything remaining

26. Object destructuring + rest

Very common in React:

const user = {
id: 1,
name: "John",
age: 30
};
const { id, ...details } = user;

Now:

id
// 1

and:

details
// { name: "John", age: 30 }

You will see patterns like this in React props.


27. A React-style example

Consider:

function User({ id, ...userDetails }) {
return (
<div>
<h2>{userDetails.name}</h2>
<p>{userDetails.email}</p>
</div>
);
}

The props object might be:

{
id: 10,
name: "John",
email: "john@example.com"
}

After:

const { id, ...userDetails } = props;

you get:

id = 10;
userDetails = {
name: "John",
email: "john@example.com"
};

This syntax becomes much easier once destructuring and rest are clear.


28. A complete example

Let’s combine everything.

const users = [
{ id: 1, name: "John", active: true, age: 30 },
{ id: 2, name: "Jane", active: false, age: 25 },
{ id: 3, name: "Mike", active: true, age: 35 }
];

Find active users

const activeUsers = users.filter(user => user.active);

Get their names

const names = activeUsers.map(user => user.name);

Get total age

const totalAge = activeUsers.reduce(
(sum, user) => sum + user.age,
0
);

Result:

activeUsers
// John, Mike
names
// ["John", "Mike"]
totalAge
// 65

The whole pipeline:

users
│
├── filter ──→ active users
│
├── map ─────→ names
│
└── reduce ──→ total age

💎 Ruby comparison

These should look familiar:

Ruby:

users.select { |user| user[:active] }

JavaScript:

users.filter(user => user.active);

Ruby:

users.map { |user| user[:name] }

JavaScript:

users.map(user => user.name);

Ruby:

users.find { |user| user[:id] == 2 }

JavaScript:

users.find(user => user.id === 2);

Ruby:

users.reduce(0) { |sum, user| sum + user[:age] }

JavaScript:

users.reduce((sum, user) => sum + user.age, 0);

The collection-processing concepts are familiar. The biggest thing to master is JavaScript’s function syntax and callback model.


⚠️ One important distinction: map does NOT modify the original array

const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
console.log(numbers);
console.log(doubled);

Output:

[1, 2, 3]
[2, 4, 6]

The original remains unchanged.

Similarly:

const filtered = numbers.filter(n => n > 1);

creates another array.

This is one reason these methods fit well with React’s preference for immutable state updates.


🎯 Int. questions

Try answering these yourself.

Q1

What’s the difference between:

forEach()
map()
filter()
find()
reduce()

Q2

What does this return?

[1, 2, 3, 4].map(n => n * 2);

Q3

What does this return?

[1, 2, 3, 4].filter(n => n % 2 === 0);

Q4

What does this return?

[10, 20, 30].find(n => n > 15);

Q5

What is the final value?

[10, 20, 30].reduce(
(sum, n) => sum + n,
0
);

Q6

What is the difference between spread and rest?

const copy = [...numbers];

versus:

function test(...numbers) {}

🧪 Live coding practice

Try solving these without using a loop first.

Problem 1 – Double numbers

doubleNumbers([1, 2, 3, 4]);
// [2, 4, 6, 8]

Use map.


Problem 2 – Active users

getActiveUsers(users);

Return only users where:

user.active === true

Use filter.


Problem 3 – Find user

findUser(users, 2);

Return the user whose id is 2.

Use find.


Problem 4 – Total

sum([10, 20, 30, 40]);
// 100

Use reduce.


Problem 5 – Method chaining

Given:

const users = [
{ name: "John", active: true },
{ name: "Jane", active: false },
{ name: "Mike", active: true },
{ name: "Sarah", active: false }
];

Produce:

["John", "Mike"]

using:

filter + map

Problem 6 – Spread

Given:

const users = ["John", "Jane"];

Create:

["John", "Jane", "Mike"]

without using push.


Problem 7 – Rest

Write:

sum(10, 20, 30, 40);

and make it return:

100

using a rest parameter.


Mental model for today

Remember these seven statements:

forEach → do something for every item

map    → transform every item

filter → keep matching items

find   → get the first matching item

some   → does at least one match?

every  → do all match?

reduce → accumulate/build one result

And:

spread → expand values
rest → collect values

The most important connection from today’s lesson is:

Array method
↓
takes a callback
↓
callback runs for each relevant item
↓
method produces a result

For example:

users
.filter(user => user.active)
.map(user => user.name);

You should now be able to read that almost as English:

“Take users, keep the active ones, then transform each remaining user into their name.”

Next lesson: Objects, destructuring, spread, this, and JavaScript’s object model. This will connect directly to React props and explain why const { name } = user and const [users, setUsers] = useState([]) look similar but work differently.

Next lesson: https://railsdrop.com/2024/12/07/javascript-es6-bootcamp-lesson-5-javascript-objects-destructuring-spread-and-this/

JavaScript / ES6+ Learning Lesson 3 – JavaScript Functions, Arrow Functions and Callbacks

This lesson is probably the most important JavaScript lesson before React.

You already know the basic idea of functions from Ruby. The big shift is:

In JavaScript, a function is also a value.

Once that clicks, callbacks, map, filter, React event handlers, useEffect, promises, and Node.js APIs become much easier to understand.

1. Normal function

The traditional syntax is:

function add(a, b) {
return a + b;
}

Call it:

const result = add(10, 20);
console.log(result); // 30

Execution:

add(10, 20)
↓
a = 10
b = 20
↓
a + b
↓
30

Very similar to Ruby:

def add(a, b)
a + b
end

2. A function is a value

This is the important part.

function add(a, b) {
return a + b;
}

You can do:

const operation = add;

Now:

operation(10, 20); // 30

Notice that we wrote:

const operation = add;

not:

const operation = add();

These are completely different.

add

Means:

Give me the function itself.

add()

Means:

Execute the function now and give me its return value.

Example:

function add() {
return 30;
}
const a = add;
const b = add();
console.log(a); // function
console.log(b); // 30

This distinction is extremely important for React.


3. Function expression

A function can also be assigned to a variable:

const add = function(a, b) {
return a + b;
};

Now:

add(10, 20);

This is called a function expression.

Compare:

function add(a, b) {
return a + b;
}

with:

const add = function(a, b) {
return a + b;
};

Both create functions, but they are written differently.


4. Arrow functions

Modern JavaScript heavily uses arrow functions.

const add = (a, b) => {
return a + b;
};

Shorter:

const add = (a, b) => a + b;

These are roughly equivalent for simple use cases:

function add(a, b) {
return a + b;
}
const add = (a, b) => a + b;

But later we’ll learn an important difference involving this.


5. Arrow function syntax

Let’s break down:

const add = (a, b) => a + b;
const add
   ↓
variable

(a, b)
   ↓
parameters

=>
   ↓
arrow function

a + b
   ↓
implicit return

Because there are no { }, JavaScript automatically returns the expression.

So:

const add = (a, b) => a + b;

means:

const add = (a, b) => {
return a + b;
};

6. Parentheses rules

With multiple parameters:

const add = (a, b) => a + b;

With one parameter, parentheses can be omitted:

const square = n => n * n;

Equivalent:

const square = (n) => n * n;

With no parameters:

const greet = () => "Hello";

So you’ll commonly see:

() => ...
x => ...
(x, y) => ...

7. Explicit vs implicit return

This is important.

Explicit return

const add = (a, b) => {
return a + b;
};

Implicit return

const add = (a, b) => a + b;

But this:

const add = (a, b) => {
a + b;
};

returns:

undefined

Why?

Because once you use { }, you have a function body. JavaScript requires an explicit:

return

if you want to return a value.


8. Returning an object from an arrow function

This is a common syntax trap.

You might write:

const createUser = () => {
name: "John"
};

That does not return the object you expect.

Use parentheses:

const createUser = () => ({
name: "John"
});

Now:

createUser();
// { name: "John" }

Explanation:

This is one of the most confusing parts of JavaScript arrow functions. The key is that {} has two different meanings depending on context.

1. Your first example

const createUser = () => {
name: "John"
};

You might read this as:

“Return an object containing name: "John".”

But JavaScript does not interpret it that way.

Because the arrow function has:

() => {
...
}

the {} is interpreted as a function block, not an object literal.

So JavaScript sees:

const createUser = () => {
name: "John";
};

as roughly:

const createUser = () => {
// function body
name: "John"; // labeled statement
// no return
};

Therefore:

console.log(createUser());

prints:

undefined

2. What is name: "John" then?

Inside a normal JavaScript block, this:

name: "John";

is interpreted as a label.

For example, JavaScript supports:

start:
for (let i = 0; i < 5; i++) {
console.log(i);
}

So in your code:

name: "John";

name is treated as a label, not an object property.

3. So why do parentheses fix it?

Now:

const createUser = () => ({
name: "John"
});

The important part is:

({
name: "John"
})

The parentheses tell JavaScript:

“Treat what’s inside these parentheses as an expression.”

Inside an expression, { name: "John" } is understood as an object literal.

So the arrow function becomes:

const createUser = () => ({
name: "John"
});

This is an implicit return.

It is basically equivalent to:

const createUser = () => {
return {
name: "John"
};
};

So:

console.log(createUser());

gives:

{ name: "John" }

4. Why can’t JavaScript just figure it out?

Therefore, JavaScript gives you two distinct forms.

Block body
const createUser = () => {
return {
name: "John"
};
};
Expression body
const createUser = () => ({
name: "John"
});

5. A useful way to remember it

Compare these:

() => {
name: "John";
}

versus:

() => ({
name: "John"
})

First:

=> { ... }
↑
function block

Second:

=> ( ... )
↑
expression
↓
object literal

One very common React example

This is why you often see:

const UserCard = () => (
<div>
<h1>John</h1>
</div>
);

The parentheses aren’t strictly required there, but they make it clear that JSX is the expression being implicitly returned.

And for objects:

const getUser = () => ({
name: "John",
age: 30
});

This is a very common JavaScript int. question because it tests whether you understand arrow functions, implicit returns, blocks and object literals.

You’ll see this frequently in React code.


9. Functions can receive functions

This is where callbacks begin.

Consider:

function execute(callback) {
callback();
}

We can call it:

execute(() => {
console.log("Hello");
});

Let’s slow this down.

The function:

() => {
console.log("Hello");
}

is passed as an argument to:

execute(...)

Inside execute:

function execute(callback) {
callback();
}

So:

execute(...)
|
| receives function
↓
callback
|
| callback()
↓
console.log("Hello")

That passed function is called a callback.


10. Why is it called a callback?

Because you’re essentially saying:

“Here is a function. Call it when you need to.”

Example:

function processUser(user, callback) {
console.log("Processing user:", user.name);
callback(user);
}

Usage:

processUser(
{ name: "John" },
(user) => {
console.log("Finished:", user.name);
}
);

The second argument is the callback.


11. Callbacks are NOT automatically asynchronous

This is very important.

Look at:

function execute(callback) {
console.log("A");
callback();
console.log("B");
}
execute(() => {
console.log("C");
});

Output:

A
C
B

Everything is synchronous.

A callback simply means:

a function passed to another function.

It does not mean asynchronous.


12. Asynchronous callback

Now:

setTimeout(() => {
  console.log("C");
}, 1000);

console.log("A");

Output:

A
C

after approximately one second.

Here the callback is asynchronous because setTimeout schedules it to run later.

This distinction is critical:

callback
≠
async

A callback may be:

synchronous
or
asynchronous

13. Higher-order functions

A function that:

  • accepts a function as an argument, or
  • returns a function

is commonly called a higher-order function.

Example:

function execute(callback) {
callback();
}

execute is a higher-order function because it accepts a function.

Another example:

function createGreeter() {
return function() {
console.log("Hello");
};
}

createGreeter is also a higher-order function because it returns a function.


14. This explains map

Now something you’ll see constantly in React:

const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);

What is actually happening?

map receives a function:

n => n * 2

Conceptually:

numbers.map(callback)
↓
function(n) {
return n * 2;
}

For every element, map calls your callback.

Conceptually:

1 → callback(1) → 2
2 → callback(2) → 4
3 → callback(3) → 6

Result:

[2, 4, 6]

15. map with objects

This is extremely important for React.

const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
const names = users.map(user => user.name);

The callback:

user => user.name

runs for every user.

Result:

["John", "Jane"]

You can think:

users
↓
map(callback)
↓
John → "John"
Jane → "Jane"
↓
["John", "Jane"]

16. filter

filter also accepts a callback.

const numbers = [1, 2, 3, 4, 5];
const result = numbers.filter(n => n > 2);

The callback returns a boolean:

1 → false
2 → false
3 → true
4 → true
5 → true

Result:

[3, 4, 5]

The pattern is:

array.filter(callback)

17. find

const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
const user = users.find(user => user.id === 2);

The callback is:

user => user.id === 2

Result:

{ id: 2, name: "Jane" }

18. Why React uses callbacks everywhere

Consider:

<button onClick={() => console.log("Clicked")}>
Click me
</button>

You’re giving React a function:

() => console.log("Clicked")

You’re essentially saying:

“When the click happens, call this function.”

That is a callback.

Another example:

users.map(user => (
<div key={user.id}>
{user.name}
</div>
))

Again:

user => (...)

is a callback.

So when you see React code filled with arrow functions, don’t think:

“React magic.”

Think:

“JavaScript is passing functions around.”

That mental model is much more useful.


19. A very important React mistake

Compare:

<button onClick={handleClick}>

and:

<button onClick={handleClick()}>

These are not the same.

First

onClick={handleClick}

means:

Give React the function. React will call it when the event occurs.

Second

onClick={handleClick()}

means:

Call the function right now and give the result to React.

This is one of the most common beginner mistakes.

Again:

function reference
handleClick
↓
"Here is the function"
function call
handleClick()
↓
"Execute it now"

20. Function parameters are just variables

Look at:

function greet(name) {
console.log(name);
}

When you call:

greet("John");

JavaScript effectively does:

name = "John"

Similarly:

function execute(callback) {
callback();
}

When:

execute(myFunction);

conceptually:

callback = myFunction

Then:

callback();

calls it.

This is the key to understanding callbacks.


21. Callback with data

Callbacks can receive values too.

function getUser(callback) {
const user = {
id: 1,
name: "John"
};
callback(user);
}

Usage:

getUser(user => {
console.log(user.name);
});

Execution:

getUser()
↓
create user
↓
callback(user)
↓
user => console.log(user.name)

Output:

John

This pattern is foundational to asynchronous JavaScript.


💎 Ruby developer comparison

You already know something conceptually similar in Ruby:

[1, 2, 3].map { |n| n * 2 }

JavaScript:

[1, 2, 3].map(n => n * 2);

Ruby:

users.map { |user| user[:name] }

JavaScript:

users.map(user => user.name);

The syntax is different, but the underlying idea is similar:

collection
↓
iterate
↓
execute supplied function for each item
↓
produce result

But don’t equate Ruby blocks and JavaScript callbacks completely. JavaScript functions are first-class values in a particularly explicit way, and the language’s treatment of closures, this, arguments and async execution differs.


Int. questions

Try these before checking the answers.

Q1

What’s the difference?

handleClick

vs

handleClick()

Q2

What does this return?

const add = (a, b) => a + b;

Q3

Why does this return undefined?

const add = (a, b) => {
a + b;
};

Q4

What is a callback?

Q5

Are callbacks always asynchronous?

Q6

Why is map considered a higher-order-function use case?

Q7

What does this produce?

const numbers = [1, 2, 3];
const result = numbers.map(n => n * 10);

Mini exercise

Predict the output:

function calculate(a, b, callback) {
const result = a + b;
callback(result);
}
calculate(10, 20, result => {
console.log(result);
});

Then trace this one:

function execute(callback) {
console.log("A");
callback();
console.log("B");
}
execute(() => {
console.log("C");
});

Expected order:

?
?
?

And finally:

const users = [
{ id: 1, name: "John", active: true },
{ id: 2, name: "Jane", active: false },
{ id: 3, name: "Mike", active: true }
];
const activeUsers = users.filter(user => user.active);
const names = activeUsers.map(user => user.name);
console.log(names);

Try to mentally execute it as:

users
  ↓
filter(callback)
  ↓
active users
  ↓
map(callback)
  ↓
names

What you should take away

At this point, you should have these mental models:

function          → a value
function()        → call the function
callback           → function passed to another function
higher-order function  → accepts/returns functions
arrow function     → concise function syntax
map/filter/find    → receive callbacks

The next lesson should build directly on this:

Lesson 4 – Arrays, map, filter, find, reduce, spread and rest, with lots of React-style examples. That lesson will make expressions like users.map(...) feel natural rather than mysterious.

Lesson 4: https://railsdrop.com/2024/12/06/javascript-es6-bootcamp-lesson-4-arrays-map-filter-find-reduce-spread-and-rest/

JavaScript / ES6+ Learning – Lesson 2

This lesson is important because JavaScript variable scope behaves differently enough from Ruby to cause bugs and int. confusion.

The four things to understand:

let
const
scope
hoisting

let, const, Scope and Hoisting

1. let vs const

Start with the simplest rule:

const name = "Abhilash";
let age = 40;

Use const when the variable should not be reassigned:

const name = "Abhilash";
// name = "John"; ❌

Use let when reassignment is required:

let age = 40;
age = 41; // ✅

Important: const does NOT make the value immutable

This is a very important JavaScript int. question.

const users = [];
users.push("John"); // ✅

The array can be modified.

What cannot happen is:

users = ["Jane"]; // ❌

Why?

Because const prevents reassignment of the variable, not mutation of the object.

Think:

const users
|
v
[ "John" ]

You cannot make users point somewhere else:

users ─────X────> [ "Jane" ]

But you can modify the existing array:

users
|
v
["John", "Jane"]

The same applies to objects:

const user = {
name: "John"
};
user.name = "Jane"; // ✅

But:

user = {}; // ❌

Int. answer

const prevents reassignment, but it does not make objects or arrays immutable.


2. What is scope?

Scope simply means:

Where can this variable be accessed?

Example:

const name = "Abhilash";
function greet() {
console.log(name);
}
greet();

name is available inside greet() because functions can access variables from their outer scope.

But:

function greet() {
const message = "Hello";
}
console.log(message); // ❌

message exists only inside the function.

Visualize:

Global Scope
│
├── name
│
└── greet()
│
└── message

greet() can see name.

The outside cannot see message.


3. Block scope

This is where let and const become important.

A block is anything inside { }, such as:

if (...)
{
}

or:

for (...)
{
}

Example:

if (true) {
const message = "Hello";
console.log(message); // ✅
}
console.log(message); // ❌

message is block scoped.

Same with let:

if (true) {
let age = 40;
}
console.log(age); // ❌

4. Why var is different

Historically JavaScript used:

var name = "John";

var does not have block scope.

Example:

if (true) {
var name = "John";
}
console.log(name); // John

That surprises many developers.

Compare:

if (true) {
let name = "John";
}
console.log(name); // ❌

This is one major reason modern JavaScript prefers let and const.


5. Ruby comparison

You may initially think:

if true
name = "John"
end
puts name

Ruby’s local-variable behavior is different from JavaScript’s block scoping rules.

For JavaScript, remember:

const + let
↓
block scoped

and:

var
↓
function scoped

You don’t need to use var in modern code, but you must understand it when reading legacy JavaScript.


6. Nested scope

Scopes can be nested.

const country = "India";
function outer() {
const state = "Kerala";
function inner() {
const city = "Kochi";
console.log(country);
console.log(state);
console.log(city);
}
inner();
}

inner() can access:

country
state
city

because it can look outward through its scope chain.

But outer() cannot access city.

Visualize:

Global
│
├── country
│
└── outer
│
├── state
│
└── inner
│
└── city

JavaScript searches for variables from the current scope outward.

This is called the scope chain.


7. A very important example

Look carefully:

const name = "Abhilash";
function greet() {
const message = `Hello ${name}`;
console.log(message);
}
greet();

Inside greet():

name

is not declared locally.

JavaScript looks outward:

greet scope
↓
global scope
↓
find name

This behavior becomes extremely important when we learn closures.


8. What is hoisting?

Now we get to a classic int. topic.

JavaScript processes declarations before executing the code in the current scope.

This behavior is commonly called hoisting.

But don’t interpret it as JavaScript literally moving your code to the top. That’s a useful mental model, but the actual mechanics are more nuanced.

Start with a function declaration:

greet();
function greet() {
console.log("Hello");
}

This works.

Why?

Function declarations are available before their textual position.


9. var and hoisting

Consider:

console.log(name);
var name = "John";

You might expect an error.

Instead:

undefined

A useful mental model is:

var name;
console.log(name);
name = "John";

The declaration is available, but the assignment happens later.


10. let and const are different

Now:

console.log(name);
let name = "John";

This produces:

ReferenceError

And:

console.log(name);
const name = "John";

also produces:

ReferenceError

This is often explained using the Temporal Dead Zone (TDZ).


11. Temporal Dead Zone

For let and const, the variable exists in the scope before its declaration is executed, but you cannot access it before that point.

Example:

console.log(age); // ❌
let age = 40;

The area between entering the scope and reaching the declaration is called the:

Temporal Dead Zone

You don’t need to memorize the implementation details yet. The int.-level mental model is:

var
↓
hoisted + initialized as undefined
let / const
↓
hoisted but inaccessible until declaration is reached

12. Function declarations vs function expressions

This becomes important when we get to callbacks.

This works:

greet();
function greet() {
console.log("Hello");
}

But this does not:

greet();
const greet = function() {
console.log("Hello");
};

And similarly:

greet();
const greet = () => {
console.log("Hello");
};

The second forms involve a const variable, so the TDZ applies.


13. A common int. trap

What does this print?

var x = 10;
if (true) {
var x = 20;
}
console.log(x);

Answer:

20

Because var is function scoped, not block scoped.

Now:

let x = 10;
if (true) {
let x = 20;
}
console.log(x);

Answer:

10

The inner x belongs to the block.

Visualize:

let x = 10
│
├── if block
│ └── let x = 20
│
└── outside -> x is still 10

14. Shadowing

JavaScript allows an inner scope to define a variable with the same name.

const name = "Abhilash";
function test() {
const name = "John";
console.log(name);
}
test();
console.log(name);

Output:

John
Abhilash

The inner variable shadows the outer one.

This is perfectly valid, although unnecessary shadowing can make code harder to read.


15. Why this matters in React

Consider:

function UserList() {
const [users, setUsers] = useState([]);
if (users.length === 0) {
const message = "No users";
return <p>{message}</p>;
}
return (
<ul>
...
</ul>
);
}

message exists only inside that if block.

React code frequently contains nested blocks, callbacks and functions, so understanding scope prevents many bugs.


16. Scope + callbacks = important later

Consider:

function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}

Don’t worry about understanding every detail yet.

The interesting question is:

How can the returned function still access count after createCounter() has finished?

That question leads directly to:

closures.

And closures are one of the most important JavaScript concepts for React and Node.js.


Qn) Is it necessary to put semi colon in Javascript?

No, putting a semicolon (or colon) at the end of a line is not mandatory in ES6 (ECMAScript 2015) or modern JavaScript.

JavaScript uses a feature called Automatic Semicolon Insertion (ASI). This means the JavaScript engine automatically inserts semicolons where it thinks they are needed to run your code correctly.

⚠️ The Rare Exception

While you can safely omit semicolons 99% of the time, there are rare cases where leaving them out can break your code. This usually happens if a line starts with a bracket [ or parenthesis (.

Example of a bug without semicolons:

const user = "Alice"
['a', 'b'].forEach(letter => console.log(letter))

The JavaScript engine reads this as a single continuous line: const user = "Alice"['a', 'b'].forEach(...), which throws an error.

Best Practice

Because of ASI, choosing to use semicolons is largely a matter of personal or team preference:

  • With semicolons: Safer for beginners, prevents accidental ASI bugs, and follows traditional coding styles.
  • Without semicolons: Keeps code looking clean, modern, and reduces visual clutter.

Most development teams use a tool like Prettier or ESLint to automatically format the code and handle semicolons for them.

Int. questions

Try answering these without looking back.

Q1

What is the difference between:

const user = {};

and:

let user = {};

Q2

Why does this work?

const users = [];
users.push("John");

even though users is declared with const?

Q3

What is the output?

let x = 10;
if (true) {
let x = 20;
}
console.log(x);

Q4

What is the output?

var x = 10;
if (true) {
var x = 20;
}
console.log(x);

Q5

What happens here?

console.log(name);
let name = "John";

Q6

Why does this work?

greet();
function greet() {
console.log("Hello");
}

but this doesn’t?

greet();
const greet = () => {
console.log("Hello");
};

Mini exercise

Predict the output before running:

const name = "Abhilash";
function outer() {
const name = "John";
if (true) {
const name = "Jane";
console.log(name);
}
console.log(name);
}
outer();
console.log(name);

Expected mental trace:

global name
↓
outer name
↓
if-block name

Check the detailed Answer here: https://railsdrop.com/is-javascript-synchronous-or-asynchronous/

The most important thing from this lesson is to build the habit of asking:

Which scope does this variable belong to?

That question will help enormously when we reach closures, callbacks, useEffect, event handlers and async JavaScript.

Next lesson: Functions in depth – function declarations, expressions, arrow functions, parameters, return values, callbacks and higher-order functions.

Lesson 3: https://railsdrop.com/2024/12/04/javascript-es6-bootcamp-lesson-3-javascript-functions-arrow-functions-and-callbacks/

JavaScript / ES6+ Learning – Lesson 1

The goal of this lesson is simple: understand what JavaScript is actually doing when you write basic code.

Coming from Ruby, you’ll notice that many concepts are similar, but JavaScript has some syntax and behavior that becomes important later in React and Node.js.

Variables, Values, Objects, Arrays, Functions and Return Values

1. Variables: let and const

In modern JavaScript, primarily use:

const name = "Abhilash";
let age = 40;

Think of them roughly like Ruby local variables:

name = "Abhilash"
age = 40

The difference is important:

const

The variable cannot be reassigned:

const name = "Abhilash";
name = "John"; // TypeError

let

The variable can be reassigned:

let age = 40;
age = 41;

What about var?

You’ll see:

var name = "Abhilash";

in older JavaScript code.

For modern JavaScript:

Use const by default.
Use let when reassignment is required.
Avoid var unless you're dealing with legacy code.

2. Everything starts with values

JavaScript variables hold values.

const name = "Abhilash";
const age = 40;
const active = true;

These values have types.

typeof name; // "string"
typeof age; // "number"
typeof active; // "boolean"

Some common types:

string
number
boolean
undefined
null
object
symbol
bigint

For now, focus on:

string
number
boolean
undefined
null
object

3. Arrays

An array stores multiple values.

const numbers = [10, 20, 30];

You can access them using an index:

numbers[0]; // 10
numbers[1]; // 20
numbers[2]; // 30

Just like Ruby:

numbers = [10, 20, 30]
numbers[0] # 10

JavaScript arrays are zero-indexed too.

Important

This:

const numbers = [10, 20, 30];

means numbers contains one array value.

It does not mean:

numbers = 10
20
30

Think:

numbers
|
v
[10, 20, 30]

4. Objects

JavaScript objects are heavily used in React and Node.

const user = {
id: 1,
name: "Abhilash",
active: true
};

You can access properties:

user.name; // "Abhilash"
user.id; // 1
user.active; // true

Ruby equivalent:

user = {
id: 1,
name: "Abhilash",
active: true
}
user[:name]

JavaScript also supports bracket notation:

user["name"];

This becomes useful when the property name is dynamic.


5. Arrays can contain objects

This is extremely common in React.

const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
{ id: 3, name: "Mike" }
];

Visualize it:

users
|
v
[
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
{ id: 3, name: "Mike" }
]

Then:

users[0].name;

returns:

"John"

This structure is everywhere in frontend applications.


6. Functions

A JavaScript function can be written like:

function add(a, b) {
return a + b;
}

Call it:

const result = add(10, 20);
console.log(result); // 30

Ruby equivalent:

def add(a, b)
a + b
end
result = add(10, 20)

So far, very familiar.


7. The important idea: functions are values

This is one of the biggest concepts you need for React and Node.

In JavaScript:

function add(a, b) {
return a + b;
}

The function itself is a value.

You can store it:

const operation = add;

Now:

operation(10, 20);

returns:

30

Visualize:

add
|
v
function
operation
|
+------> same function

This is why JavaScript can pass functions around so easily.


8. Functions can return anything

A function can return a number:

function getAge() {
return 40;
}

A string:

function getName() {
return "Abhilash";
}

An object:

function getUser() {
return {
id: 1,
name: "John"
};
}

An array:

function getUsers() {
return [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
}

And importantly, a function can return another function:

function createGreeter() {
return function() {
console.log("Hello");
};
}

We’ll use this concept later when learning closures.


9. This explains useState

Now let’s return to the example that confused you:

const [users, setUsers] = useState([]);

Ignore React for a moment.

Imagine an ordinary function:

function useState(initialValue) {
return [
initialValue,
function setValue(value) {
console.log(value);
}
];
}

Now call:

const result = useState([]);

What does result contain?

Conceptually:

[
[],
setValueFunction
]

So:

const users = result[0];
const setUsers = result[1];

Now combine the two operations with destructuring:

const [users, setUsers] = result;

Therefore:

const [users, setUsers] = useState([]);

means:

1. Pass [] into useState
2. useState returns [value, function]
3. destructure that returned array
4. users gets element 0
5. setUsers gets element 1

This is a critical mental model.


10. Array destructuring

Let’s isolate the JavaScript feature.

const numbers = [10, 20];
const [a, b] = numbers;

Equivalent to:

const a = numbers[0];
const b = numbers[1];

You can even ignore values:

const [first, , third] = [10, 20, 30];
console.log(first); // 10
console.log(third); // 30

11. Object destructuring

There is another extremely common form:

const user = {
name: "John",
age: 30
};
const { name, age } = user;

Equivalent to:

const name = user.name;
const age = user.age;

So remember:

Array -> []
Object -> {}

Therefore:

const [a, b] = array;
const { name, age } = object;

This distinction is extremely important in React.


12. Arrow functions

Modern JavaScript frequently uses:

const add = (a, b) => {
return a + b;
};

Short form:

const add = (a, b) => a + b;

Equivalent roughly to:

function add(a, b) {
return a + b;
}

React code uses arrow functions constantly:

users.map(user => user.name);

Don’t worry about map yet. Just notice:

user => user.name

is a function.


13. Why callbacks matter

Consider:

function execute(callback) {
callback();
}

Then:

execute(() => {
console.log("Hello");
});

What’s happening?

execute()
|
| receives a function
v
callback
|
v
callback()
|
v
console.log("Hello")

The function is being passed as a value.

This is the foundation of callbacks.

We’ll go much deeper into this in a later lesson.


Ruby developer mental model

For now, keep these mappings in your head:

JavaScriptRuby
const x = 10x = 10
[]Array
{}Hash-like object
functiondef / Proc/Lambda concepts
returnreturn
obj.nameobj[:name] for Hash
array[0]array[0]
function as valueProc/lambda/block-like concept
arrow functionlambda-ish syntax, but not identical

The last row is deliberately approximate. JavaScript functions, arrow functions, Ruby blocks, Procs and lambdas are not interchangeable concepts. We’ll cover the differences properly.


🎯 Lesson 1 int. questions

Try answering these without looking back.

Question 1

What does this return?

const numbers = [10, 20, 30];
numbers[1];

Question 2

What is the difference between:

const user = {
name: "John"
};

and:

const users = [
{ name: "John" }
];

Question 3

What does this do?

const [a, b] = [10, 20];

Question 4

What does this return?

function getUser() {
return {
id: 1,
name: "John"
};
}

Question 5

What is stored in operation?

function add(a, b) {
return a + b;
}
const operation = add;

Small exercise

Without running it, predict the output:

function getUser() {
return [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
}
const users = getUser();
const [firstUser, secondUser] = users;
console.log(firstUser.name);
console.log(secondUser.name);

Then try this variation:

const [firstUser] = getUser();
console.log(firstUser.name);

The key skill I’m looking for is not memorizing syntax. It’s being able to mentally trace:

function call
↓
return value
↓
array/object
↓
destructuring
↓
variables

Once that becomes natural, a lot of React code will stop looking mysterious.

Next lesson: let, const, scope, hoisting, and why JavaScript behaves differently from Ruby around variable scope.

Happy Learning! to be continued..

Senior-Level Linux Commands Every Backend Engineer Should Know

For a senior backend engineer, Linux commands are more than tools for navigating directories.

They become a production debugging language.

When a Rails application is consuming too much memory, a log contains millions of lines, a deployment introduces unexpected configuration changes, or a background worker suddenly starts failing, knowing how to combine commands such as sed, awk, grep, find, xargs, sort, uniq, cut, tr, jq, and ps can save hours.

The real skill is not memorizing commands. It is understanding how to compose them into pipelines.

command1 | command2 | command3

This article focuses on commands and techniques that become particularly valuable at a senior engineering level.


1. grep – Search With Intent

Most developers know:

grep "ERROR" production.log

But grep becomes much more powerful with regular expressions and recursive searches.

Search recursively

grep -R "ActiveRecord::Deadlocked" log/

Useful when you don’t know which file contains the problem.

Ignore case

grep -Ri "timeout" .

Show line numbers

grep -n "connection refused" production.log

Search multiple patterns

grep -E "ERROR|FATAL|Exception" production.log

Show context around matches

grep -C 5 "NoMethodError" production.log

This is extremely useful for application logs because the surrounding lines often contain request IDs, parameters, stack traces, or timestamps.

When to use

Use grep when your primary operation is:

“Find lines matching this condition.”

2. sed – Stream Editing

sed is one of the most useful Linux commands for manipulating text without opening an editor.

The simplest example:

sed 's/foo/bar/g' file.txt

Replace every foo with bar.

Delete lines

Delete empty lines:

sed '/^$/d' file.txt

Delete lines containing DEBUG:

sed '/DEBUG/d' production.log

Print specific lines

sed -n '100,150p' production.log

This displays lines 100 through 150.

Very useful when investigating a specific portion of a huge log file.

Modify a configuration file

For example:

sed -i 's/RAILS_LOG_LEVEL=info/RAILS_LOG_LEVEL=debug/' .env

-i modifies the file in place.

Be careful with production configuration files. Prefer making a backup when appropriate:

sed -i.bak 's/old_value/new_value/g' config.yml

Advanced use: remove sensitive information

Suppose logs contain email addresses:

User login: john@example.com
User login: alice@example.com

We can mask them:

sed -E 's/[[:alnum:]._%+-]+@[[:alnum:].-]+\.[A-Za-z]{2,}/[REDACTED]/g' app.log

This is useful when sanitizing logs before sharing them.

When to use sed

Think:

“I want to transform or filter text while streaming it.”

3. awk – Lightweight Data Processing

awk is one of the most important commands for senior engineers.

It treats input as structured columns.

Suppose:

101 John 4500
102 Alice 6000
103 Bob 5000

Run:

awk '{print $1, $3}' users.txt

Output:

101 4500
102 6000
103 5000

Filter records

awk '$3 > 5000 {print $1, $2, $3}' users.txt

Now only users earning more than 5000 are printed.

Calculate values

awk '{sum += $3} END {print sum}' users.txt

Calculate the total salary.

Average:

awk '{sum += $3; count++} END {print sum/count}' users.txt

Processing logs

Imagine an Nginx log:

10.0.0.1 GET /users 200
10.0.0.2 GET /users 500
10.0.0.3 GET /products 200
10.0.0.4 GET /users 500

Extract HTTP status:

awk '{print $4}' access.log

Count status codes:

awk '{print $4}' access.log | sort | uniq -c

Result:

2 200
2 500

awk with conditions

awk '$4 >= 500 {print}' access.log

Find server errors.

When to use awk

Think:

“My input has columns/records and I need to filter, transform, aggregate, or calculate something.”

For quick operational data analysis, awk can often replace writing a small script.

4. cut – Extract Columns

For simple column extraction, cut is usually easier than awk.

Example:

cut -d',' -f1 users.csv

Extract the first CSV field.

Multiple fields:

cut -d',' -f1,3 users.csv

Character ranges:

cut -c1-10 file.txt

Use cut when the operation is straightforward.

Use awk when logic becomes conditional or computational.

5. sort + uniq – Finding Patterns

These commands become extremely powerful together.

Suppose you want to find the most common URLs:

awk '{print $7}' access.log |
sort |
uniq -c |
sort -nr

Example:

1500 /api/users
980 /api/orders
450 /health

This is a classic production-analysis pipeline.

Why sort before uniq?

uniq only detects adjacent duplicate lines.

Therefore:

sort file.txt | uniq

is usually required.

6. head and tail – Inspect Large Files Safely

Instead of opening a 10 GB log:

head -n 50 production.log

Last 100 lines:

tail -n 100 production.log

The real power is:

tail -f production.log

Follow new log entries in real time.

For Rails applications this is particularly useful during deployments:

tail -f log/production.log

You can combine it with grep:

tail -f production.log | grep --line-buffered "ERROR"

Now you’re effectively monitoring errors as they occur.

7. find – Locate Files Precisely

Find Ruby files:

find app/ -type f -name "*.rb"

Find files modified recently:

find log/ -type f -mtime -1

Find large files:

find /var/log -type f -size +500M

Find and execute a command:

find tmp/ -type f -name "*.tmp" -delete

Be careful with destructive commands.

A safer approach is:

find tmp/ -type f -name "*.tmp" -print

Inspect the result first.

8. xargs – Turn Output Into Arguments

Suppose:

find tmp/ -type f -name "*.tmp"

returns many files.

You can pass them to another command:

find tmp/ -type f -name "*.tmp" -print0 |
xargs -0 rm

-print0 and -0 are important because filenames can contain spaces or special characters.

Another example:

grep -Rl "TODO" app/ | xargs wc -l

This finds files containing TODO and counts their lines.

9. ps – Understand Running Processes

For a Rails server:

ps aux | grep puma

More useful:

ps aux --sort=-%mem | head

Find processes consuming the most memory.

CPU:

ps aux --sort=-%cpu | head

This can quickly identify runaway workers.

10. top and htop – Live System Diagnosis

top

For an easier interactive interface:

htop

Use these when diagnosing:

  • High CPU
  • Memory pressure
  • Load
  • Runaway processes
  • Number of workers
  • Process states

For application debugging, don’t look only at Rails logs. Always correlate application behavior with OS-level resource usage.

11. df vs du

These commands answer different questions.

Disk filesystem usage

df -h

Answers:

How full is the filesystem?

Directory usage

du -sh log/

Answers:

What is consuming the space?

Find the largest directories:

du -sh * | sort -hr | head

This is extremely useful when a server suddenly reports:

No space left on device

12. lsof – Discover Who Owns a Resource

Find which process is using port 3000:

lsof -i :3000

Find processes using a file:

lsof /var/log/production.log

Find deleted files still consuming disk:

lsof +L1

This last one is particularly valuable.

A process may keep a deleted log file open. du may not show the file anymore, while disk space remains consumed until the process releases it.

13. ss – Network Investigation

Modern Linux systems commonly use ss for socket inspection.

Check listening ports:

ss -lntp

Check established connections:

ss -nt

Find connections to port 5432:

ss -nt | grep ':5432'

This can help investigate:

  • PostgreSQL connection exhaustion
  • Unexpected network connections
  • Services not listening
  • Connection buildup

14. jq – JSON From the Command Line

Modern APIs produce JSON everywhere.

Suppose:

{
"users": [
{"id": 1, "name": "John"},
{"id": 2, "name": "Alice"}
]
}

Extract names:

jq '.users[].name' response.json

Output:

"John"
"Alice"

Transform it:

jq -r '.users[] | "\(.id),\(.name)"' response.json

This becomes especially powerful when debugging APIs:

curl -s https://example.com/api/users |
jq '.users[] | select(.active == true)'

15. curl – API Debugging From the Shell

Instead of immediately reaching for Postman:

curl -i https://example.com/health

POST JSON:

curl -X POST https://example.com/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John"}'

Measure request timing:

curl -o /dev/null -s \
-w 'HTTP: %{http_code}\nTime: %{time_total}s\n' \
https://example.com

This is extremely useful when debugging production APIs.

16. tee – See and Save Output Simultaneously

bundle exec rails db:migrate 2>&1 | tee migration.log

The output is displayed on the terminal while simultaneously being written to a file.

Useful during deployments and troubleshooting.

17. Powerful Pipelines

The real senior-level skill comes from combining commands.

For example, identify the most frequent 500 responses:

grep " 500 " access.log |
awk '{print $7}' |
sort |
uniq -c |
sort -nr |
head -20

Or find the largest log files:

find /var/log -type f -size +100M -print |
xargs -r ls -lh |
sort -k5 -hr

Or monitor Rails errors:

tail -f log/production.log |
grep --line-buffered -E "ERROR|FATAL|Exception"

18. A Practical Senior Engineer Mental Model

Instead of memorizing hundreds of commands, categorize them.

RequirementCommands
Searchgrep, rg
Transform textsed
Process columns/dataawk, cut
Count/group datasort, uniq
Locate filesfind
Connect commandsxargs, pipes
Inspect processesps, top, htop
Inspect disksdf, du
Inspect socketsss, lsof
JSON processingjq
HTTP/API debuggingcurl
Save + display outputtee

The most important progression is:

Basic Linux
↓
Individual commands
↓
Pipelines
↓
Conditional filtering
↓
Aggregation
↓
Production diagnosis

A senior engineer should be comfortable turning an unclear operational question into a shell pipeline.

For example:

“Which API endpoints are causing the most HTTP 500 errors right now?”

Instead of manually opening a log file, you should naturally arrive at something like:

grep " 500 " access.log |
awk '{print $7}' |
sort |
uniq -c |
sort -nr |
head -20

That is the real power of Linux:

small, composable tools solving complex operational problems.

For Rails engineers especially, mastering these commands means you can diagnose the application, process, filesystem, network, and logs from the same shell instead of relying entirely on application-level tooling.

Happy commanding!

Ruby Coding Problems – Part 1: Basics (String & Array)

Common patterns: iteration, hashing, two-pointer, basic transformations. Try each yourself first – ask for solutions/hints per question when ready.

  1. Reverse a string without using .reverse
    reverse_string("hello") => "olleh"
  2. Palindrome check
    palindrome?("racecar") => true
    palindrome?("hello") => false
  3. Count vowels
    count_vowels("programming") => 3
  4. Find max in array without .max
    find_max([3, 7, 2, 9, 4]) => 9
  5. Remove duplicates from array, preserve order
    remove_duplicates([1,2,2,3,1,4]) => [1,2,3,4]
  6. FizzBuzz (1 to n)
    fizzbuzz(15) => ["1","2","Fizz","4","Buzz",...,"FizzBuzz"]
  7. Anagram check
    anagram?("listen", "silent") => true
  8. Sum of array, no .sum
    array_sum([1,2,3,4]) => 10
  9. Capitalize each word (title case), no .capitalize on whole string
    title_case("the ruby language") => "The Ruby Language"
  10. Find second largest number
    second_largest([4, 1, 9, 7, 9]) => 7
  11. Find Alice and Bob spending amounts
    details = [{ amount: 2500, requestor: 'Alice', id: 23 }...
  12. Pattern Check
    pattern_check("{()}") # => true
  13. Rotate the given String
    rotate('angel', 4) # => "lange"

1. Reverse a string without using .reverse

Concept

1. finding the last string character index to find the last string character first

2. then decreasing the index to find the upto the first character

def reverse_string(str)
  last_str_index = str.length - 1
  result = ""
  
  while last_str_index >= 0
    result << str[last_str_index]

    last_str_index -= 1
  end

  result
end

puts reverse_string("hello")
puts reverse_string("programming")

Solution 2: Another way without using Index variables

def reverse_string(str)
  str.each_char.reduce("") { |result, char| char + result }
end

Concept

Prepend each character to an accumulator instead of appending – that flips the order without touching any index. each_char + reduce replaces the while-loop/counter entirely.

2. Palindrome check

def palindrome?(str)
  first_char_index = 0
  last_char_index = str.length - 1

  while first_char_index < last_char_index
    if str[first_char_index] != str[last_char_index]
      return false 
    end

    first_char_index += 1
    last_char_index -= 1
  end

  return true
end

p palindrome?("ala")
p palindrome?("alla")
p palindrome?("racecar")
p palindrome?("car")

Concept

  1. Two pointer approach

Two indices start at opposite ends of the string and move toward each other, comparing elements pairwise:

  • first_char_index starts at 0, last_char_index starts at length - 1
  • Each iteration compares str[first] vs str[last] – if they ever mismatch, it can’t be a palindrome, so return immediately
  • Otherwise, both pointers move inward (first += 1, last -= 1) until they meet or cross (first < last becomes false)
  • If the loop finishes without a mismatch, all mirrored pairs matched → palindrome

Why this pattern in general: it’s the go-to when you need to compare elements from both ends of a sequence without extra space – palindromes, reversing in-place, “sorted array pair sum” problems, container/water-trapping problems all reuse this exact skeleton (two indices, converge or diverge, one comparison per step).

One edge case worth saying out loud in an interview: this correctly handles even-length (abba) and odd-length (aba, middle char never gets compared to itself) without any special-casing – that’s often a follow-up question.

3. Count vowels

1. Without Using Array#each or any other Enumerable methods

def count_vowels(str)
  vowels = ['a', 'e', 'i', 'o', 'u']
  vowel_count = 0
  first = 0 

  while first <= str.length - 1
    if vowels.include?(str[first])
      vowel_count += 1
    end

    first += 1
  end

  vowel_count
end


p count_vowels("programming")
p count_vowels("ala")
p count_vowels("Grow your audience by promoting your content")

Concept

  1. Using a pointer

2. Using Enumerable#count

# using `Enumerable#count`

def count_vowels(str)
  vowels = ['a', 'e', 'i', 'o', 'u']

  str.each_char.count { |char| vowels.include?(char.downcase) }
end

p count_vowels("programming")
p count_vowels("ala")
p count_vowels("Grow your audience by promoting your content A")

Enumerable#count

The Enumerable#count method in Ruby returns the number of elements in a collection, optionally filtering them based on an item or a truthy block criterion.

[10, 20, 30].count 
# => 3

{ a: 1, b: 2 }.count 
# => 2

[1, 2, 4, 2, 1, 2].count(2) 
# => 3

["apple", "banana", "apple"].count("apple") 
# => 2

# Count numbers greater than 10
[5, 12, 8, 18, 3].count { |num| num > 10 } 
# => 2

# Count odd numbers using symbol-to-proc syntax
[1, 2, 3, 4, 5].count(&:odd?) 
# => 3

# With a Hash, it yields both the key and the value
{ candy: 5, apples: 2, cookies: 10 }.count { |key, value| value > 4 } 
# => 2

3. Using Enumerable#select

# using `String#each_char` and `Enumerable#select`

def count_vowels(str)
  vowels = ['a', 'e', 'i', 'o', 'u']
  
  str.each_char.select { |char| vowels.include?(char.downcase) }.size
end

p count_vowels("programming")
p count_vowels("ala")
p count_vowels("Grow your audience by promoting your content A")

In Ruby, Enumerable#select (Aka filter, find_all) is an inbuilt method used to filter a collection by evaluating each element against a given block and returning only the items for which the block evaluates to true.

collection.select { |element| condition }

With a block: Returns a new collection containing all elements that match the condition.
Without a block: Returns an Enumerator object.
Aliases: filter and find_all are exact aliases and perform identically

reject –> The opposite of select; returns all elements that evaluate to false.

find / detect –> Returns only the first element that matches the condition, then stops iterating.

## Array
numbers = [1, 2, 3, 4, 5, 6]

# Using block syntax to get even numbers
even_numbers = numbers.select { |num| num.even? }
# => [2, 4, 6]

# Short-hand symbol-to-proc syntax
even_numbers = numbers.select(&:even?)
# => [2, 4, 6]

## Hash
scores = { alice: 95, bob: 65, charlie: 82 }

# Filter for scores greater than 70
passing = scores.select { |name, score| score > 70 }
# => {:alice=>95, :charlie=>82}

users = [
  { name: "Alice", active: true },
  { name: "Bob", active: false },
  { name: "Charlie", active: true }
]

active_users = users.select { |user| user[:active] }
# => [{:name=>"Alice", :active=>true}, {:name=>"Charlie", :active=>true}]

4. Using Array#each

In Ruby, each is not actually a method defined by the Enumerable module itself; instead, it is a method requirement that your class must implement.

The Enumerable module acts as a mixin that provides collection-handling capabilities (like .map, .select and .reduce). However, for those methods to function, your custom collection class must define its own #each method to yield items sequentially.

# using `String#split` and `Array#each`

def count_vowels(str)
  vowels = ['a', 'e', 'i', 'o', 'u']
  vowel_count = 0
  
  str.split('').each do |char|  
    vowel_count += 1 if vowels.include?(char) 
  end

  vowel_count
end

p count_vowels("programming")
p count_vowels("ala")
p count_vowels("Grow your audience by promoting your content")

Best: Solution 2 (each_char.count)

def count_vowels(str)
vowels = ['a', 'e', 'i', 'o', 'u']
str.each_char.count { |char| vowels.include?(char.downcase) }
end

count with a block is built exactly for “how many elements satisfy this predicate” – it says what you want, not how to accumulate it. One line of actual logic, no throwaway accumulator variable. This is what a senior Ruby dev would write.

Why the others rank lower:

  • Solution 1 (your index/while version): correct, but same critique as Q1 – manual pointer + counter for something Enumerable does in one call. Fine as a “let me show I understand the mechanics” opener, but don’t lead with it if asked for idiomatic Ruby.
  • Solution 3 (select.size): works, but wasteful – select builds an intermediate array just to throw it away and count its size. count does the same job without the allocation. Small thing, but an interviewer watching for efficiency awareness will notice.
  • Solution 4 (split('').each): split('') allocates a full array up front; each_char iterates lazily without materializing one. Also reintroduces the manual counter that count eliminates. Weakest of the four.

4. Find max in array without .max

# using `Array#each`

def find_max(array)
  max = nil

  array.each do |num|
    max = num if max.nil? || num > max
  end
  
  max
end

p find_max([])
p find_max([3, 7, 2, 9, 4])
p find_max([32, 7, 29, 79, 41])

This is actually the idiomatic version – no index needed since each gives you the values directly, and seeding max with nil (instead of array[0] or 0) correctly handles edge cases: empty array returns nil instead of crashing or silently returning wrong data, and it works for negative-only arrays where seeding with 0 would be a bug.

Concept:

  1. linear scan with running accumulator

Track the best-seen-so-far value in a variable, compare each new element against it, update when you find something better. This is the base pattern behind max/min, and generalizes directly to “find the element matching some condition” problems (max by custom criteria, longest string, etc.).

One thing worth saying in an interview: this is O(n) time, O(1) space and it’s actually not worse than Array#max – that’s what .max does internally too.

Only nitpick: num > max – if you want strict correctness on the first iteration, walk through it: max is nil, max.nil? short-circuits true, so num > max never evaluates against nil (which would raise). Good – that’s intentional short-circuit ordering, not luck. Just make sure you can explain why the order of the || matters if asked.

5. Remove duplicates from array, preserve order

Using Array#uniq

def remove_duplicates(array)
  array.uniq
end

remove_duplicates([1,2,2,3,1,4]) => [1,2,3,4]

Without using uniq, select etc.

At First I tried to iterate over array using array index and deleting the duplicated value, then pass the mutated array recursively into the Method. This cause issue like: mutating array (via .delete) while iterating over it with each_with_index – the index no longer matches the shrinking array, so later lookups go out of bounds and return nil.

X - WRONG
array.each_with_index do |num, index|
other_nums = array[(index + 1)..-1]
other_nums.each do |next_num|
if num == next_num # duplicate num
uniq_nums << array.delete(num) # store duplicated
# find duplicate without duplicated num
remove_duplicates(array, uniq_nums)
end
end
end
uniq_nums + array

Nested loops (which is what pushed me to O(n²) and the mutation trap in the first place)

General rule: never mutate a collection you’re actively iterating over. This is a classic bug.

Fix – hash-based “seen” tracker, single pass, no uniq/select:

def remove_duplicates(array)
  seen = {}

  array.each do |num|
    seen[num] = true unless seen[num]
  end

  seen.keys
end

p remove_duplicates([1, 2, 2, 3, 1, 4, 3])
p remove_duplicates([7, 4, 2, 7, 2, 8, 4])
p remove_duplicates([8, 1, 0, 8, 0, 0, 1, 5, 6])

Concept:

  1. seen-set / membership tracking

Use a hash as an O(1) lookup table for “have I encountered this before?” instead of nested loops. Single pass:

  • New value → mark it seen, keep it
  • Already seen → skip it, original order preserved naturally since you only append once per unique value

This pattern is the backbone of dedup, “first unique element” – same seen/counts hash idea reused everywhere. No recursion needed;

6. FizzBuzz (1 to n)

The Fizz Buzz problem requires writing a program that prints or returns numbers from 1 to a given integer n, replacing multiples of 3 with “Fizz”, multiples of 5 with “Buzz”, and multiples of both 3 and 5 with “FizzBuzz”.

https://leetcode.com/problems/fizz-buzz/description

The Rules

For every integer i from 1 to n:

  • Print “FizzBuzz” if i is divisible by both 3 and 5 (i.e., a multiple of 15).
  • Print “Fizz” if i is divisible only by 3.
  • Print “Buzz” if i is divisible only by 5.
  • Print the number itself as a string if none of the above conditions match.

Example (n = 15)

If n = 15, the output sequence looks like this:
1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"

def fizzbuzz(limit)
  result = []
  (1..limit).each do |num|
      if num % 15 == 0
        result << "FizzBuzz"
      elsif num % 3 == 0
        result << "Fizz"
      elsif num % 5 == 0
        result << "Buzz"
      else
        result << num.to_s
      end
  end
  
  result
end

p fizzbuzz(15)
p fizzbuzz(30)

Concept:

  1. range iteration + conditional branching, no state carried between iterations (unlike Q5’s seen hash) – each number is judged independently, so a simple each with if/elsif is the correct tool, nothing fancier needed.

One idiomatic variant worth knowing for interviews, using map instead of manual array-building:

def fizzbuzz(limit)
  (1..limit).map do |num|
    if num % 15 == 0
      "FizzBuzz"
    elsif num % 3 == 0
      "Fizz"
    elsif num % 5 == 0
      "Buzz"
    else
      num.to_s
    end
  end
end

7. Anagram check

def anagram?(first, second)
  return false unless first.length == second.length

  first_count_hash = Hash.new(0)
  second_count_hash = Hash.new(0)

  first.each_char {|char| first_count_hash[char] += 1 }
  second.each_char {|char| second_count_hash[char] += 1 }

  first_count_hash == second_count_hash
end

p anagram?("listen", "silent")
p anagram?("note", "tone")
p anagram?("act", "cat")
p anagram?("earth", "heart")
p anagram?("earth", "hears")

Fix – frequency count comparison (no sort, no mutation, no uniq/tally even):

CONcept

  1. frequency-count comparison

Two strings are anagrams iff they have identical character-frequency distributions. Build a count hash for each string (Hash.new(0) gives a default of 0 so += 1 works without checking key? first), then compare the two hashes directly – Ruby’s Hash#== checks all key-value pairs match, regardless of insertion order.

This is the same “seen/counts” hash idea from Q5, reused again – frequency-count hashes are one of the most repeated tools across string/array interview problems (anagrams, first-unique-char, “group anagrams,” character-frequency questions). Worth internalizing as your default first move whenever a problem involves counting occurrences of something.

8. Sum of array, no .sum

Using Array#inject

def array_sum(array)
  array.inject(:+)
end

Without using Array#inject

# without using .sum, .inject
def array_sum(array)
  return "provide non-empty array" if array.empty?

  sum = 0
  array.each do |num|
    sum += num
  end

  sum
end

p array_sum([1,2,3,4])
p array_sum([2,72,1,0])

Correct Solution. each with an accumulator is the right idiom here – this is one of the few cases where a keeping-total variable isn’t a code smell, because you genuinely need to carry state (the sum) across iterations, unlike Q6’s FizzBuzz where each element was independent.

Concept:

  1. accumulator pattern

Since I am avoiding .sum/.inject here specifically, Good to say explicitly: “In production I’d use .sum; writing it manually to show the mechanics.”

9. Capitalize each word (title case), no .capitalize on whole string

def title_case(sentence)
  sentence.split.map { |word| word[0].upcase + word[1..] }.join(' ')
end

p title_case("the ruby language")

Concept:

  1. split → transform each element → rejoin. split (whitespace-aware, collapses multiple spaces automatically) → map for a 1-to-1 word transform (same reasoning as Q6’s FizzBuzz – independent per-element transform, no accumulator needed) → join to reassemble.

10. Find second largest number

# this has one BUG - check below

def second_largest(array)
  return nil if array.length < 2

  second_largest = array.first
  largest = array.first

  array[1..].each do |num|
    if num > largest
      second_largest = largest
      largest = num
    elsif num < largest && num > second_largest
      second_largest = num
    end
  end

  second_largest
end


p second_largest([4, 1, 9, 7, 9])
p second_largest([4, 3, 2, 3, 4])
p second_largest([50, 100, 150, 200])

Concept:

  1. single-pass dual-tracking – same accumulator idea as Q4/Q8, but tracking two running values instead of one, with an ordering dependency between them (you can only correctly update second_largest relative to where largest currently stands, which is why the elsif must re-check against both bounds, not just one).

BUG FOUND:

second_largest([4, 3, 2, 3, 4]) => 4 X WRONG - Why?

Good catch – real bug. Walk through it:

largest = second_largest = 4 (both seeded to array.first)

The real problem: initializing both trackers to the same value creates a chicken-and-egg lockout – second_largest can only be beaten by something bigger, but it started at the max, so nothing (except a new largest) can ever unseat it.

Fix – seed with -Infinity, don’t assume array.first is a valid second-place candidate:

# FIXED

def second_largest(array)
  return nil if array.length < 2

  largest = second_largest = -Float::INFINITY

  array.each do |num|
    if num > largest
      second_largest = largest
      largest = num
    elsif num > second_largest && num != largest
      second_largest = num
    end
  end

  second_largest
end

Concept refinement:

  1. dual-tracker pattern (Q10 original) + -Float::INFINITY, not real array values – this is the standard idiom for “find min/max/second-max” problems specifically because it guarantees the first comparison always succeeds and updates correctly – good to be able to answer each one individually if an interviewer pushes on “why did you add that condition?”

NOTE: This is a good one to remember: never seed a running-max/min tracker with an actual data element unless you’re certain it can’t create a lockout — -Infinity/nil-then-check (like your Q4 find_max) are the safe patterns.

11. Find Alice and Bob spending amount from orders

Qn) Find the total amount spend by Alice and Bob

details = [
  { amount: 2500, requestor: 'Alice', id: 23 },
  { amount: 2500, requestor: 'John', id: 22 },
  { amount: 1500, requestor: 'Bob', id: 21 },
  { amount: 1500, requestor: 'Alice', id: 23 },
  { amount: 1000, requestor: 'Bob', id: 21 },
  { amount: 500, requestor: 'Sera', id: 20 }
]

Answer:

# Answer 1 (filter + each + accumulator)

result = Hash.new(0)
filters = ["Alice", "Bob"]
details.filter { |order| filters.include?(order[:requestor]) }
       .each {|order| result[order[:requestor]] += order[:amount]  } 

puts result
# Answer 2 (filter + group_by + each_pair + reduce + accumulator)

filters = ["Alice", "Bob"]

result = []

details.filter {|order| filters.include?(order[:requestor]) }
       .group_by {|order| order[:requestor]}
       .each_pair { |user, orders| result << { "#{user}": orders.reduce(0) {|sum, order| sum += order[:amount] } } }

puts result

12. Pattern check

Qn) Find the expression pattern match correctly with words included in it

# my solution

def pattern_check(pattern = "")
  return true if pattern.empty?

  pairs_hash = {
    "{" => "}",
    "(" => ")",
    " " => " "
  }

  symbols = pattern.split("")
  last_index = symbols.size - 1
  first_half = last_index / 2

  # p symbols

  idx = 0
  while (idx <= first_half)
    symbol = symbols[idx]
    pair = symbols[last_index - idx]

    # p "symbol: #{symbol}"
    # p "pair index: #{last_index - idx}"
    # p "static pair symbol: #{pairs_hash[symbol]}"
    # p "pair symbol received: #{pair}"
    
    if symbol.match(/\w+/) && pair.match(/\w+/)
      idx += 1
      next
    end

    unless pairs_hash[symbol] == pair
      return false
    end
    
    idx += 1
  end

  return true
end

p pattern_check("{()}") # => true

p pattern_check("{(})") # => false

p pattern_check("{( text )}") # => true

The above solution is based on Two Pointer approach and is not correct.

Check for the correct solution (Stack Approach) here: https://railsdrop.com/what-the-question-is-actually-asking/

12. Rotate the given String

Qn) Rotate the string characters n times, n is the position given.


def rotate(str, position)
  initial_pos = 0
  while initial_pos < position
    str << str[0] && str[0] = ""
    
    initial_pos += 1
  end
  
  str
end

name = "angel"

p rotate(name, 4)
=> "lange"

Learning C to Understand Ruby – Part 2: Memory, Pointers and the Ruby Object Model

In Part 1, I looked at why learning C can be valuable for a Ruby developer-not to replace Ruby, but to understand what happens underneath it.

This time, we go closer to the machine.

The concepts are simple:

memory, addresses, pointers, stack, heap.

But they completely change the way you think about Ruby objects.


Everything ultimately becomes memory

Consider this Ruby code:

name = "Ruby"

At the Ruby level, we think:

name → "Ruby"

At the machine level, however, something must exist in memory.

There is storage for the string’s data, metadata describing the object, and some mechanism for Ruby to refer to that object.

The exact representation is an implementation detail, but the important idea is:

Ruby objects ultimately have a physical representation in memory.

C lets us see memory directly.


Memory has addresses

Consider:

int number = 42;

The variable has a value:

42

but it also occupies some location in memory.

We can ask C for that location:

printf("%p", (void *)&number);

The & operator means:

Give me the address of number.

You might see something like:

0x7ffee1234abc

The actual address is not important.

The concept is.

Memory
0x7ffee1234abc
↓
[42]

Now we have crossed an important boundary.

We are no longer thinking only about values.

We are thinking about where those values live.


A pointer stores an address

C lets us store that address:

int number = 42;
int *ptr = &number;

Now:

number
↓
[42]
ptr
↓
[address of number]

And:

printf("%d", *ptr);

The * dereferences the pointer.

It means:

Go to the address stored in ptr and access the value there.

So:

*ptr = 100;

changes the original variable:

number = 100

This is one of C’s defining characteristics.

You can explicitly work with addresses and the data behind them.


Ruby references are not C pointers

This is an important distinction.

Ruby variables behave somewhat like references from a conceptual perspective, but Ruby does not expose raw memory addresses and pointer arithmetic in normal Ruby code.

For example:

name = "Ruby"
other = name

You can think:

name
│
└────→ String object
other
│
└────→ same String object

But Ruby does not let you simply say:

"Take this address and add 8 bytes."

C does.

That difference is fundamental.

Ruby gives you an object model.

C gives you memory-level primitives from which many such abstractions can be built.


Stack and heap

Now we reach another important concept.

A running program uses memory in different ways. Two areas you’ll encounter immediately are the stack and the heap.

Consider:

void calculate() {
int number = 42;
}

The local variable has automatic storage associated with the function’s execution.

Conceptually:

Stack
calculate()
┌───────────────┐
│ number = 42 │
└───────────────┘

When the function returns, that stack storage is no longer needed.

Dynamic allocation is different:

int *number = malloc(sizeof(int));
*number = 42;

Now memory is allocated dynamically.

Conceptually:

Stack
┌───────────────┐
│ number │──────┐
└───────────────┘ │
↓
Heap
┌────────┐
│ 42 │
└────────┘

And C expects you to eventually release it:

free(number);

This explicit ownership model is one of the biggest differences between C and Ruby.

C always need to know, how large a piece of data is and where do I put it. Everything in C is something like: name + address + value


Ruby Memory Management – JIT Comparision

Check: https://docs.ruby-lang.org/en/3.4/yjit/yjit_md.html


Ruby’s heap becomes a much more interesting subject

In Ruby, you normally write:

user = User.new

and never ask:

Who called malloc?
Where exactly is this object?
Who will release its memory?

Ruby’s runtime manages those details.

The object is allocated under Ruby’s memory-management system and the garbage collector tracks object reachability and determines when memory can be reclaimed.

So rather than:

Application → malloc → free

you generally experience:

Ruby code
↓
Ruby runtime
↓
allocation
↓
Ruby heap
↓
GC

Learning C makes that second model much easier to reason about.


The fascinating part: VALUE

Now we arrive at one of the concepts that makes CRuby internals especially interesting.

In CRuby, Ruby values are represented internally using a type called:

VALUE

You will encounter VALUE everywhere when reading the Ruby C implementation and C extension APIs.

Conceptually, you can think of it as:

the low-level representation Ruby uses to pass around Ruby values inside the runtime.

For example, a Ruby C API function may look conceptually like:

VALUE rb_str_new_cstr(const char *ptr);

and C extension methods often receive and return VALUEs.

That means your Ruby object:

"hello"

does not remain some abstract concept all the way down.

CRuby represents it using its internal object/value machinery.


Not every Ruby value is simply a pointer

This is where Ruby becomes particularly interesting.

A common beginner assumption is:

Ruby object = pointer to heap object

That’s useful as a rough mental model, but it isn’t the whole story.

CRuby uses a representation that can encode certain immediate values directly rather than allocating a separate heap object for every value.

Integers are a classic example.

So when you write:

number = 42

you shouldn’t automatically imagine:

number
↓
heap object containing 42

The runtime has specialized representations for some Ruby values.

This is one reason looking at CRuby internals is so educational.

A high-level statement such as:

“Ruby variables point to objects”

is useful, but the implementation is much more nuanced.


Why this matters for a Ruby developer

Let’s take:

a = 10
b = 10

At the Ruby language level, you care that both variables represent the integer 10.

After learning some C and Ruby internals, you start asking different questions:

Are these separate objects?
Is 10 heap allocated?
How does CRuby represent integers?
How does Ruby distinguish integers from ordinary heap objects?
What exactly is stored in VALUE?

Those are much deeper questions.

And they lead directly into:

  • immediate values
  • object flags
  • object headers
  • pointer tagging
  • garbage collection
  • object allocation
  • Ruby’s internal data structures

Pointers explain something else: object identity

Ruby lets us ask:

a = Object.new
b = a
a.equal?(b)
# => true

Why?

Because both variables refer to the same object.

Conceptually:

a ─────┐
↓
[Object]
↑
│
b ─────┘

C gives you the vocabulary to understand this relationship:

reference
address
pointer
memory location

Again, Ruby intentionally hides the actual pointer from application code.

But the underlying concept of “multiple references to the same object” remains.


The danger of C is also the lesson

Ruby protects you from many classes of memory errors.

In C, you can easily write:

int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
*ptr = 100;

Now you’re accessing memory after it has been released.

That’s a use-after-free.

You can also leak memory:

int *ptr = malloc(sizeof(int));
/* forgot free(ptr) */

Or write outside an allocated buffer:

int numbers[10];
numbers[100] = 42;

These bugs are difficult precisely because C gives you so much control.

And that is the paradox:

The freedom that makes C powerful is the same freedom that makes it dangerous.

Ruby takes many of these responsibilities away from you.


The real payoff

After learning these concepts, this Ruby code:

users = 10_000.times.map { User.new }

starts looking different.

Instead of only seeing:

Ruby objects

you can begin thinking:

Ruby objects
↓
object representation
↓
memory allocation
↓
references
↓
Ruby heap
↓
garbage collector

And when a Rails application starts consuming hundreds of megabytes of memory, that mental model becomes much more useful.

You can ask better questions.

Not just:

“Why is Rails using so much memory?”

but:

“What objects are being allocated, how long do they remain reachable, and how does Ruby’s allocator and GC interact with that workload?”

That’s a much more senior-level way of investigating the problem.


Where we go next

We have now established the foundation:

C
↓
Memory
↓
Addresses
↓
Pointers
↓
Stack / Heap
↓
Ruby references
↓
VALUE
↓
CRuby object representation

The next step gets even more interesting:

What does a Ruby object actually look like inside CRuby?

We’ll look at concepts such as object headers, RBasic, type information, flags, heap allocation, and how the garbage collector sees Ruby objects.

That’s where the gap between:

User.new

and:

VALUE obj;

starts to disappear.

Happy Learning! 🚀

Learning C to Understand Ruby: A Senior Ruby Developer’s Journey – Part 1

As a Ruby developer, I have spent years enjoying one of Ruby’s biggest strengths: abstraction.

In Rails, I can write:

users = User.where(active: true)

and focus on the business problem rather than memory allocation, pointers, system calls or CPU instructions.

That is exactly why Ruby is productive.

But recently, I started asking a different question:

What is actually happening underneath my Ruby code?

What happens when Ruby creates an object?
Where does that object live?
Who allocates the memory?
Who releases it?
What does an array really look like internally?
What happens when Ruby calls a method?

And that leads to an interesting realization:

Learning C is not necessarily about moving away from Ruby. It can be a way of understanding Ruby at a much deeper level.

This is the first part of that journey.


Ruby hides the machine – intentionally

Consider this:

user = User.new

At the Ruby level, this is trivial.

But conceptually, a lot more is happening.

Ruby needs to:

  1. Represent the object.
  2. Allocate memory for it.
  3. Initialize its internal state.
  4. Keep track of the object for garbage collection.
  5. Maintain references between objects.
  6. Eventually reclaim its memory.

Ruby handles these details for us.

That abstraction is one of the reasons we love Ruby.

But it also means that most Ruby developers don’t need to think about the actual machine.

C removes much of that abstraction.


C forces you to think about memory

In C, you quickly encounter things like:

int number = 42;

and:

int *ptr = &number;

The second line introduces a concept that Ruby normally keeps away from you: the memory address of a value.

You can explicitly allocate memory:

int *numbers = malloc(100 * sizeof(int));

and explicitly release it:

free(numbers);

That changes your mental model.

Instead of thinking only in terms of:

objects
methods
classes

you begin thinking about:

memory
addresses
bytes
layouts
allocation
lifetime
references

And this is extremely useful when trying to understand Ruby internally.


Ruby objects are still data in memory

Take a simple Ruby value:

name = "Abhilash"

As a Ruby developer, you normally think:

name → String

A lower-level mindset makes you ask:

name
  ↓
Ruby value/reference
  ↓
Object representation
  ↓
Memory
  ↓
Bytes

Ruby doesn’t magically escape the laws of computing.

At some point, that string has to exist in memory.

The same is true for:

Array
Hash
Integer
String
User

They all ultimately have machine-level representations.

Learning C helps you become curious about those representations.


Stack vs Heap

One of the first concepts worth learning in C is the difference between stack and heap memory.

For example:

void example() {
    int number = 10;
}

The local variable has automatic storage duration associated with the function’s execution.

Dynamic allocation looks different:

int *number = malloc(sizeof(int));
*number = 10;

free(number);

Now the program explicitly controls the allocation and lifetime.

This distinction is extremely important when later studying Ruby’s memory management.

Ruby objects are managed by the runtime rather than by application code using malloc and free directly.

That leads naturally to the next question:

Who manages Ruby’s heap?

The answer takes us into the Ruby garbage collector.


Garbage collection becomes much easier to understand

A Ruby developer typically learns:

“Ruby has a garbage collector, so I don’t need to manually free objects.”

That’s correct, but incomplete.

Once you understand manual memory management in C, garbage collection becomes much more interesting.

You can start thinking about:

Object allocation
       ↓
Heap
       ↓
References
       ↓
Object becomes unreachable
       ↓
Garbage collector
       ↓
Memory can be reclaimed

Instead of viewing GC as some magical Ruby feature, you begin seeing it as a runtime memory-management strategy.

That distinction is important.

Ruby didn’t eliminate memory management.

It automated memory management.


C also teaches you that data layout matters

Consider:

struct User {
    int id;
    char name[50];
};

You are explicitly describing a data structure’s layout.

You begin thinking about questions such as:

  • How many bytes does this structure occupy?
  • How are fields aligned?
  • Are objects contiguous?
  • How efficiently will the CPU access them?
  • What happens to cache locality?

Ruby normally shields you from these concerns.

But when performance suddenly matters, these concepts become valuable.

For example, processing millions of objects isn’t only about algorithmic complexity.

Memory access patterns can matter too.

This is one reason understanding low-level systems concepts can make you a better high-level developer.


Then there is the most interesting part: Ruby itself uses C

This is where the journey becomes particularly relevant to Ruby developers.

The standard Ruby implementation, CRuby, is largely implemented in C.

That means the language we write:

array.map(&:name)

eventually reaches a runtime implemented at a much lower level.

Conceptually:

Ruby code
   ↓
Ruby parser / VM
   ↓
CRuby runtime
   ↓
Operating system
   ↓
CPU / memory

Once you start reading Ruby’s C source code, concepts that initially look mysterious start becoming understandable:

VALUE
Ruby objects
references
object allocation
method dispatch
garbage collection
VM execution

And suddenly C stops being just another programming language.

It becomes a lens through which you can inspect Ruby itself.


Why should a senior Rails developer care?

You don’t need to write your next Rails application in C.

That isn’t the point.

The goal is to develop a deeper mental model.

When you write:

100_000.times do
User.new
end

you should eventually be able to think beyond the Ruby syntax.

You start wondering:

How many allocations?

Where are those objects stored?

How does GC discover them?

What references exist?

How much memory is being consumed?

What happens when these objects become unreachable?

What is the runtime doing while my Ruby code executes?

Those questions are far more valuable than memorizing another Rails API.


The goal of this journey

My objective isn’t:

“Become a C programmer.”

It is:

Become a Ruby developer who understands what Ruby is doing underneath.

And the roadmap becomes surprisingly clear:

C fundamentals
      ↓
Pointers & memory
      ↓
Stack & heap
      ↓
Processes & system calls
      ↓
C programming at system level
      ↓
CRuby internals
      ↓
Ruby VM
      ↓
Garbage collection
      ↓
Ruby C extensions

The interesting part is that the deeper you go into C, the less mysterious Ruby becomes.

Ruby’s abstractions don’t disappear.

You simply start seeing what is behind them.

And for me, that is the real power of learning C as a Ruby developer.

Part 2 will start with the most important foundation: memory, pointers, stack, heap and how these concepts map to the Ruby object model.

For Part 2, I’d make memory + pointers + stack/heap → Ruby objects. That is where this series can become genuinely fascinating for an experienced Ruby developer.

Happy Learning! 🚀