Why Ruby begin Block with ensure is Important

Ruby provides a powerful way to handle exceptions using the begin block. One of the key features of this block is ensure, which ensures that a certain section of code runs no matter what happens in the begin block. This is particularly useful when dealing with resource management, such as file handling, database connections, and network requests.

Understanding begin, rescue, and ensure

The begin block in Ruby is used to handle potential exceptions. It works alongside rescue, which catches exceptions, and ensure, which executes code regardless of whether an exception occurs.

Basic Syntax:

begin
  # Code that might raise an exception
rescue SomeError => e
  # Handle the exception
ensure
  # Code that will always execute
end

Why is ensure Important?

  1. Guaranteed Execution – Code inside ensure runs no matter what, ensuring cleanup actions always occur.
  2. Resource Cleanup – Ensures that resources like file handles, database connections, or network sockets are properly closed.
  3. Prevents Leaks – Helps avoid memory or resource leaks by making sure cleanup is performed.

Example 1: File Handling

One of the most common uses of ensure is closing a file after performing operations.

file = nil
begin
  file = File.open("example.txt", "r")
  puts file.read
rescue StandardError => e
  puts "An error occurred: #{e.message}"
ensure
  file.close if file
  puts "File closed."
end

Explanation:

  • The begin block opens a file and reads its contents.
  • If an error occurs (e.g., file not found), the rescue block catches it.
  • The ensure block ensures that the file is closed, preventing resource leaks.

Example 2: Database Connection Handling

Handling database connections properly is crucial to avoid locked or hanging connections.

require 'sqlite3'

db = nil
begin
  db = SQLite3::Database.open("test.db") # Open database connection
  db.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
  db.execute("INSERT INTO users (name) VALUES ('Alice')")
  puts "User added successfully."
rescue SQLite3::Exception => e
  puts "Database error: #{e.message}"
ensure
  db.close if db # Ensure the database connection is closed
  puts "Database connection closed."
end

Explanation:

  • Opens a database connection and executes SQL statements.
  • If an error occurs, such as a syntax error in SQL, rescue catches it.
  • The ensure block ensures the database connection is closed, preventing connection leaks.

Example 3: Network Request Handling

When making HTTP requests, errors like timeouts or invalid URLs can occur. Using ensure, we can ensure proper handling.

require 'net/http'

url = URI("http://example.com")
response = nil

begin
  response = Net::HTTP.get(url)
  puts "Response received: #{response[0..50]}..." # Print a snippet of the response
rescue StandardError => e
  puts "Network error: #{e.message}"
ensure
  puts "Request complete. Cleanup actions (if any) can be performed here."
end

Explanation:

  • Makes an HTTP request to a given URL.
  • If an error occurs (e.g., network failure), rescue handles it.
  • The ensure block ensures any necessary final actions, such as logging, happen.

Key Takeaways

  • The ensure block always executes, making it essential for cleanup tasks.
  • It helps prevent resource leaks by ensuring proper closure of files, database connections, and network requests.
  • Using ensure makes your Ruby code robust and reliable, handling errors gracefully while ensuring necessary actions take place.

By incorporating ensure in your Ruby code, you can improve reliability, maintainability, and efficiency in handling critical resources.

Can We Do Type Checking in Ruby Method Parameters?

Ruby is a dynamically typed language that favors duck typing over strict type enforcement. However, there are cases where type checking can be useful to avoid unexpected behavior. In this post, we’ll explore various ways to perform type validation and type checking in Ruby.

Type Checking and Type Casting in Ruby

Yes, even though Ruby does not enforce types at the language level, there are several techniques to validate the types of method parameters. Below are some approaches:

1. Manual Type Checking with raise

One straightforward way to enforce type checks is by manually verifying the type of a parameter using is_a? and raising an error if it does not match the expected type.

def my_method(arg)
  raise TypeError, "Expected String, got #{arg.class}" unless arg.is_a?(String)
  
  puts "Valid input: #{arg}"
end

my_method("Hello")  # Works fine
my_method(123)      # Raises: TypeError: Expected String, got Integer

2. Using respond_to? for Duck Typing

Rather than enforcing a strict class type, we can check whether an object responds to a specific method.

def my_method(arg)
  unless arg.respond_to?(:to_str)
    raise TypeError, "Expected a string-like object, got #{arg.class}"
  end
  
  puts "Valid input: #{arg}"
end

my_method("Hello")  # Works fine
my_method(:symbol)  # Raises TypeError

3. Using Ruby 3’s Type Signatures (RBS)

Ruby 3 introduced RBS and TypeProf for static type checking. You can define types in an .rbs file:

def my_method: (String) -> void

Then, you can use tools like steep, a static type checker for Ruby, to enforce type checking at development time.

How to Use Steep for Type Checking

Steep does not use annotations or perform type inference on its own. Instead, it relies on .rbi files to define type signatures. Here’s how you can use Steep for type checking:

  1. Define a Ruby Class:
class Calculator
  def initialize(value)
    @value = value
  end
  
  def double
    @value * 2
  end
end

  1. Generate an .rbi File:
steep scaffold calculator.rb > sig/calculator.rbi

This generates an .rbi file, but initially, it will use any for all types. You need to manually edit it to specify proper types.

  1. Modify the .rbi File to Define Types:
class Calculator
  @value: Integer
  def initialize: (Integer) -> void
  def double: () -> Integer
end

  1. Run Steep to Check Types:
steep check

Steep also supports generics and union types, making it a powerful but less intrusive type-checking tool compared to Sorbet.

4. Using Sorbet for Stronger Type Checking

Sorbet is a third-party static type checker that allows you to enforce type constraints at runtime.

require 'sorbet-runtime'

extend T::Sig

sig { params(arg: String).void }
def my_method(arg)
  puts "Valid input: #{arg}"
end

my_method("Hello")  # Works fine
my_method(123)      # Raises error at runtime

References:

Another Approach: Using Rescue for Type Validation

A different way to handle type checking is by using exception handling (rescue) to catch unexpected types and enforce validation.

def process_order(order_items, customer_name, discount_code)
  # Main logic
  ...

rescue => e
  # Type and validation checks
  raise "Expecting an array of items: #{order_items.inspect}" unless order_items.is_a?(Array)
  raise "Order must contain at least one item: #{order_items.inspect}" if order_items.empty?
  raise "Expecting a string for customer name: #{customer_name.inspect}" unless customer_name.is_a?(String)
  raise "Customer name cannot be empty" if customer_name.strip.empty?
  
  raise "Unexpected error in `process_order`: #{e.message}"
end

Summary

  • Use is_a? or respond_to? for runtime type checking.
  • Use Ruby 3’s RBS for static type enforcement.
  • Use Sorbet for stricter type checking at runtime.
  • Use Steep for static type checking with RBS.
  • Exception handling can be used for validating types dynamically.

Additional Considerations

Ruby is a dynamically typed language, and unit tests can often be more effective than type checks in ensuring correctness. Writing tests ensures that method contracts are upheld for expected data.

For Ruby versions prior to 3.0, install the rbs gem separately to define types for classes.

If a method is defined, it will likely be called. If reasonable tests exist, every method will be executed and checked. Therefore, instead of adding excessive type checks, investing time in writing tests can be a better strategy.

Installing ⚙️ and Setting Up 🔧 Ruby 3.4, Rails 8.0 and IDE on macOS in 2025

Ruby on Rails is a powerful framework for building web applications. If you’re setting up your development environment on macOS in 2025, this guide will walk you through installing Ruby 3.4, Rails 8, and a best IDE for development.

1. Installing Ruby and Rails

“While macOS comes with Ruby pre-installed, it’s often outdated and can’t be upgraded easily. Using a version manager like Mise allows you to install the latest Ruby version, switch between versions, and upgrade as needed.” – Rails guides

Install Dependencies

Run the following command to install essential dependencies (takes time):

brew install openssl@3 libyaml gmp rust

…..
==> Installing rust dependency: libssh2, readline, sqlite, python@3.13, pkgconf
==> Installing rust

zsh completions have been installed to:
/opt/homebrew/share/zsh/site-functions
==> Summary
🍺 /opt/homebrew/Cellar/rust/1.84.1: 3,566 files, 321.3MB
==> Running brew cleanup rust…
==> openssl@3
A CA file has been bootstrapped using certificates from the system
keychain. To add additional certificates, place .pem files in
/opt/homebrew/etc/openssl@3/certs

and run
/opt/homebrew/opt/openssl@3/bin/c_rehash
==> rust
zsh completions have been installed to:
/opt/homebrew/share/zsh/site-functions

Install Mise Version Manager

curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc
source ~/.zshrc

Install Ruby and Rails

mise use -g ruby@3
mise ruby@3.4.1 ✓ installed
mise ~/.config/mise/config.toml tools: ruby@3.4.1

ruby --version   # output Ruby 3.4.1

gem install rails

# reload terminal and check
rails --version  # output Rails 8.0.1

For additional guidance, refer to these resources:


2. Installing an IDE for Ruby on Rails Development

Choosing the right Integrated Development Environment (IDE) is crucial for productivity. Here are some popular options:

RubyMine

  • Feature-rich and specifically designed for Ruby on Rails.
  • Includes debugging tools, database integration, and smart code assistance.
  • Paid software that can be resource-intensive.

Sublime Text

  • Lightweight and highly customizable.
  • Requires plugins for additional functionality.

Visual Studio Code (VS Code) (Recommended)

  • Free and open-source.
  • Excellent plugin support.

Install VS Code

Follow the official installation guide.

Enable GitHub Copilot for AI-assisted coding:

  1. Open VS Code.
  2. Sign in with your GitHub account.
  3. Enable Copilot from the extensions panel.

To use VS Code from the terminal, ensure code is added to your $PATH:

  1. Open Command Palette (Cmd+Shift+P).
  2. Search for Shell Command: Install 'code' command in PATH.
  3. Restart your terminal and try: code .

3. Your 15 Essential VS Code Extensions for Ruby on Rails

To enhance your development workflow, install the following VS Code extensions:

  1. GitHub Copilot – AI-assisted coding (already installed).
  2. vscode-icons – Better file and folder icons.
  3. Tabnine AI – AI autocompletion for JavaScript and other languages.
  4. Ruby & Ruby LSP – Language support and linting.
  5. ERB Formatter/Beautify – Formats .erb files (requires htmlbeautifier gem): gem install htmlbeautifier
  6. ERB Helper Tags – Autocomplete for ERB tags.
  7. GitLens – Advanced Git integration.
  8. Ruby Solargraph – Provides code completion and inline documentation (requires solargraph gem): gem install solargraph
  9. Rails DB Schema – Auto-completion for Rails database schema.
  10. ruby-rubocop – Ruby linting and auto-formatting (requires rubocop gem): gem install rubocop
  11. endwise – Auto-adds end keyword in Ruby.
  12. Output Colorizer – Enhances syntax highlighting in log files.
  13. Auto Rename Tag – Automatically renames paired HTML/Ruby tags.
  14. Highlight Matching Tag – Highlights matching tags for better visibility.
  15. Bracket Pair Colorizer 2 – Improved bracket highlighting.

Conclusion

By following this guide, you’ve successfully set up a robust Ruby on Rails development environment on macOS. With Mise for version management, Rails installed, and VS Code configured with essential extensions, you’re ready to start building Ruby on Rails applications.

Part 2: https://railsdrop.com/2025/03/22/setup-rails-8-app-rubocop-actiontext-image-processing-part-2

Happy Rails setup! 🚀

Setting Up Terminal 🖥️ for Development on MacOS (Updated 2025)

If you’re setting up your MacBook for development, having a well-configured terminal is essential. This guide will walk you through installing and configuring a powerful terminal setup using Homebrew, iTerm2, and Oh My Zsh, along with useful plugins.

1. Install Homebrew

Homebrew is a package manager that simplifies installing software on macOS.

Open the Terminal and run:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

After installation, add Homebrew to your PATH by running the following commands:

echo >> ~/.zprofile
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

Verify installation:

brew --version

Check here.

2. Install iTerm2

The default macOS Terminal is functional but lacks advanced features. iTerm2 is a powerful alternative.

Install it using Homebrew:

brew install --cask iterm2

Open iTerm2 from your Applications folder after installation.

Check and Install Git

Ensure Git is installed:

git --version

If not installed, install it using Homebrew:

brew install git

3. Install Oh My Zsh

Oh My Zsh enhances the Zsh shell with themes and plugins. Install it with:

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

Check here.

Configure .zshrc

Edit your .zshrc file:

vim ~/.zshrc

Add useful plugins:

plugins=(git rails ruby)

The default theme is robbyrussell. You can explore other themes here.

Customize iTerm2 Color Scheme

Find and import themes from iTerm2 Color Schemes.

4. Add Zsh Plugins

Enhance your terminal experience with useful plugins.

a. Install zsh-autosuggestions

This plugin provides command suggestions as you type.

Install via Oh My Zsh:

git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions

Or install via Homebrew:

brew install zsh-autosuggestions

Add to ~/.zshrc:

plugins=(git rails ruby zsh-autosuggestions)

If installed via Homebrew, add:

source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh

to the bottom of ~/.zshrc.

Restart iTerm2:

exec zsh

b. Install zsh-syntax-highlighting

This plugin highlights commands to distinguish valid syntax from errors.

Install via Oh My Zsh:

git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

Add to .zshrc:

plugins=(git rails ruby zsh-autosuggestions zsh-syntax-highlighting)

Restart iTerm2:

exec zsh

Wrapping Up

Your terminal is now set up for an optimized development experience! With Homebrew, iTerm2, Oh My Zsh, and useful plugins, your workflow will be faster and more efficient.

to be continued …

The Evolution of Asset 📑 Management in Web and Ruby on Rails

Understanding Middleware in Rails

When a client request comes into a Rails application, it doesn’t always go directly to the MVC (Model-View-Controller) layer. Instead, it might first pass through middleware, which handles tasks such as authentication, logging, and static asset management.

Rails uses middleware like ActionDispatch::Static to efficiently serve static assets before they even reach the main application.

ActionDispatch::Static Documentation

“This middleware serves static files from disk, if available. If no file is found, it hands off to the main app.”

Where Are Static Files Stored?

Rails stores static assets in the public/ directory, and ActionDispatch::Static ensures these are served efficiently without hitting the Rails stack.

Core Components of Ruby on Rails – A reminder

To understand asset management evolution, let’s quickly revisit Rails’ core components:

  • ActiveRecord: Object-relational mapping (ORM) system for database interactions.
  • Action Pack: Handles the controller and view layers.
  • Active Support: A collection of utility classes and standard library extensions.
  • Action Mailer: A framework for designing email services.

The Role of Browsers in Asset Management

Web browsers cache static assets to improve performance. The caching strategy varies based on asset types:

  • Images: Rarely change, so they are aggressively cached.
  • JavaScript and CSS files: Frequently updated, requiring cache-busting mechanisms.

The Era of Sprockets

Historically, Rails used Sprockets as its default asset pipeline. Sprockets provided:

  • Conversion of CoffeeScript to JavaScript and SCSS to CSS.
  • Minification and bundling of assets into fewer files.
  • Digest-based caching to ensure updated assets were fetched when changed.

The Rise of JavaScript & The Shift Towards Webpack

The release of ES6 (2015-2016) was a turning point for JavaScript, fueling the rise of Single Page Applications (SPAs). This marked a shift from traditional asset management:

  • Sprockets was effective but became complex and difficult to configure for modern JS frameworks.
  • Projects started including package.json at the root, indicating JavaScript dependency management.
  • Webpack emerged as the go-to tool for handling JavaScript, offering features like tree-shaking, hot module replacement, and modern JavaScript syntax support.

The Landscape in 2024: A More Simplified Approach

Recent advancements in web technology have drastically simplified asset management:

  1. ES6 Native Support in All Major Browsers
    • No need for transpilation of modern JavaScript.
  2. CSS Advancements
    • Features like variables and nesting eliminate the need for preprocessors like SASS.
  3. HTTP/2 and Multiplexing
    • Enables parallel loading of multiple assets over a single connection, reducing dependency on bundling strategies.

Enter Propshaft: The Modern Asset Pipeline

Propshaft is the new asset management solution introduced in Rails, replacing Sprockets for simpler and faster asset handling. Key benefits include:

  • Digest-based file stamping for effective cache busting.
  • Direct and predictable mapping of assets without complex processing.
  • Better integration with HTTP/2 for efficient asset delivery.

Rails 8 Precompile Uses Propshaft

What is Precompile? A Reminder

Precompilation hashes all file names and places them in the public/ folder, making them accessible to the public.

Propshaft improves upon this by creating a manifest file that maps the original filename as a key and the hashed filename as a value. This significantly enhances the developer experience in Rails.

Propshaft ultimately moves asset management in Rails to the next level, making it more efficient and streamlined.

The Future of Asset Management in Rails

With advancements like native ES6 support and CSS improvements, Rails continues evolving to embrace simpler, more efficient asset management strategies. Propshaft, combined with modern browser capabilities, makes asset handling seamless and more performance-oriented.

As the web progresses, we can expect further simplifications in asset pipelines, making Rails applications faster and easier to maintain.

Stay tuned for more innovations in the Rails ecosystem!

Happy Rails Coding! 🚀

JavaScript / ES6+ Bootcamp 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" }

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 func → 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.

JavaScript / ES6+ Bootcamp – 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

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+ Bootcamp – 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!

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! 🚀