Comments #

Comments are the part of your code that the interpreter doesn’t execute — but that doesn’t mean they’re unimportant. Good comments are the difference between code you can understand in seconds and code that makes other developers (or yourself three months from now) spend hours just trying to figure out what it means. In Ruby, there are two comment syntaxes: single-line and multi-line. But far more important than the syntax is when and how to write comments that actually add value. This article covers both, including the RDoc and YARD documentation systems used across the professional Ruby ecosystem.

Single-Line Comments #

Single-line comments are the most commonly used comment form in Ruby. They start with a hash sign (#) and apply until the end of that line — the Ruby interpreter ignores all text after # on the same line.

# This is a single-line comment — the whole line is ignored by the interpreter

x = 10  # This is an inline comment — the code on the left still executes

A single-line comment can sit on its own line before the code, or at the end of a code line (inline comment). Both are valid, but they serve different purposes.

# Calculate the total price after applying the discount
total = base_price * (1 - discount)

tax = total * 0.11   # 11% VAT has been in effect since April 2022

A comment on its own line is a good fit for explaining an upcoming block of code. An inline comment works better for contextual notes that are very specific to a single expression — like a magic number that needs explanation.

Temporarily Disabling Code #

One practical use of comments is temporarily disabling lines of code during debugging, without deleting them.

def process_payment(order)
  validate_stock(order)
  # send_notification_email(order)   # temporarily disabled — see issue #412
  deduct_balance(order)
  create_invoice(order)
end
Comments that disable code should be temporary. If a line of code has been commented out for more than a few days with no clear plan for when it will be re-enabled, chances are it’s simply no longer needed. Just delete it — version control (Git) keeps the history.

Multi-Line Comments #

When an explanation is too long for a single line, Ruby provides two ways to write comments that span multiple lines.

Using =begin and =end #

Ruby has a dedicated syntax for multi-line comments using the =begin and =end pair. All text between these two markers is ignored by the interpreter.

=begin
This module handles the entire user authentication flow,
including login, logout, token refresh, and session validation.

Dependencies:
  - JWT gem for token encryption
  - BCrypt for password hashing
  - Redis for active session storage

Author: Backend Team
Last updated: 2024-03-15
=end

module AuthHandler
  # implementation...
end

There’s one strict rule to follow: =begin and =end must be at the start of the line — no spaces or any other characters before them. If they’re indented, Ruby won’t recognize them as comment markers.

def example
  =begin
  ANTI-PATTERN: this will NOT work as a comment!
  =begin with indentation in front isn't recognized by Ruby.
  This line will actually cause a SyntaxError.
  =end
end

Because of this limitation, =begin...=end is almost never used inside a method or class body. It’s most appropriate at the top level of a file — for file headers, license notices, or module descriptions.

Using Multiple Single-Line Comments #

In real-world practice, a far more common way to write long comments is stacking several # comments in sequence. This is more flexible because it can be placed anywhere, including inside methods and classes.

# This search algorithm uses a binary search approach
# with O(log n) time complexity. The given array
# MUST be sorted ascending before this method is called.
#
# If the array isn't sorted, use LinearSearch as an alternative
# even though its complexity is O(n).
def binary_search(arr, target)
  low  = 0
  high = arr.length - 1

  while low <= high
    mid = (low + high) / 2

    if arr[mid] == target
      return mid        # index found
    elsif arr[mid] < target
      low = mid + 1     # search the right half
    else
      high = mid - 1    # search the left half
    end
  end

  -1  # target not found
end

Notice the empty # line above — this is a widely used convention for separating paragraphs within a long comment, making the comment easier to scan visually.

flowchart TD
    A[Need to write a long comment?] --> B{Inside a method\nor class body?}
    B -- Yes --> C["Use multiple consecutive #"]
    B -- No --> D{Is this a file header\nor license?}
    D -- Yes --> E["=begin...=end or\nmultiple # both fine"]
    D -- No --> C
    C --> F["Separate paragraphs\nwith an empty # line"]
    E --> G[Place at the\ntop of the file]

The Philosophy of Good Comments #

This is the most overlooked yet most important part: not how to write comments syntactically, but what deserves a comment.

Explain Why, Not What #

Good code already explains what it’s doing. A comment that merely repeats what’s obvious from the code is noise — it lengthens the file without adding understanding.

# ANTI-PATTERN: comments that repeat the code — no added value
# Adding 1 to the counter
counter += 1

# Checking whether the user is active
if user.active?
  # ...
end

# CORRECT: comments that explain WHY — provide context
counter += 1  # compensate for the 0-based offset of the third-party API

if user.active?
  # Only active users can access this endpoint.
  # Suspended users are still in the DB but must not be able to log in.
end

Comment Non-Obvious Decisions #

The most valuable comments are those that explain design decisions that aren’t obvious — why you chose approach A over B, why a particular magic number exists, or why there’s a workaround that looks strange.

# Use sleep 0.5 here because the payment gateway API requires
# a minimum 500ms gap between two consecutive requests from the same IP.
# Without this pause, the second request gets rate-limited and fails with 429.
# Reference: https://docs.paymentgw.com/rate-limiting
sleep 0.5
response = gateway.charge(amount)

# This weight formula comes from Nielsen's (1994) paper on
# usability heuristics. 0.1 for minor, 0.5 for major,
# 1.0 for catastrophic — validated with internal user testing.
final_score = (minor * 0.1) + (major * 0.5) + (catastrophic * 1.0)

TODO and FIXME Comments #

Ruby (and many editors/IDEs) recognizes comments with special tags that are useful for marking unfinished work.

# TODO: add email format validation before the v2.0 release
# FIXME: this method crashes if the input is an empty string — see issue #88
# HACK: temporary workaround for a bug in net-http library version 0.3.x
# NOTE: behavior differs on Windows due to CRLF vs LF line ending differences
# OPTIMIZE: this query is slow for datasets >100k rows, consider indexing

def save_user(email, name)
  # FIXME: no duplicate email check yet
  User.create(email: email, name: name)
end
When to use special comment tags:
  ✓ TODO  → planned work that hasn't been done yet
  ✓ FIXME → known bug that must be fixed
  ✓ HACK  → temporary solution that isn't ideal but intentionally left
  ✓ NOTE  → important information the next developer should notice
  ✓ OPTIMIZE → working code whose performance needs improvement

  ✗ Don't use TODO as a substitute for tickets in an issue tracker
  ✗ Don't let FIXMEs pile up without following up

Comment Anti-Patterns to Avoid #

# ANTI-PATTERN 1: comments out of sync with the code
# Calculating a 10% discount
discount = price * 0.15   # ✗ the code says 15% but the comment says 10%

# ANTI-PATTERN 2: comments that tell history — use Git
# Used to use bubble sort but switched to quicksort on 12 Jan 2023
# for better performance. Previously tried merge sort but the memory
# footprint was too large for big datasets.
def sort(arr)
  arr.sort   # just use the built-in and write a descriptive commit message in Git
end

# ANTI-PATTERN 3: unnecessary closing-block comments
class UserService
  def initialize
    # ...
  end # def initialize   ← not needed, Ruby is obvious
end # class UserService   ← not needed for a short class

# CORRECT: closing comments are only useful for VERY long blocks
# where the screen can't show the opening and closing at once
# ANTI-PATTERN 4: commented-out code with no explanation
def process_data(input)
  # result = input.map { |x| x * 2 }
  # result = result.select { |x| x > 10 }
  result = input.filter_map { |x| x * 2 if x * 2 > 10 }
  result
end
# ✗ Commented-out code without context is confusing —
#   is this intentionally kept? Why not just delete it?

# CORRECT: if you really need to keep it, provide context
def process_data(input)
  # Older, more explicit version — kept for reference
  # in case filter_map misbehaves on Ruby < 2.7:
  # result = input.map { |x| x * 2 }
  # result = result.select { |x| x > 10 }
  result = input.filter_map { |x| x * 2 if x * 2 > 10 }
  result
end

RDoc — Ruby’s Standard Documentation #

RDoc is Ruby’s built-in documentation system. When you run ri Array or read the docs at ruby-doc.org, the output you see is generated from the RDoc comments in Ruby’s own source code. In other words, RDoc comments are how you write documentation that’s readable by both machines and humans.

RDoc reads # comments placed directly above a class, module, or method declaration — with no blank line in between.

Documenting Classes and Modules #

# Manages CRUD operations for the Product entity in the inventory system.
#
# This class is responsible for product data validation, database
# persistence, and synchronization with the external warehouse system.
#
# == Usage Example
#
#   product = Product.new(name: "Laptop", price: 15_000_000)
#   product.save
#   puts product.id  # => 1
#
# == Notes
#
# This class is not thread-safe. Use a Mutex if accessed from
# multiple threads concurrently.
class Product
  # implementation...
end

Documenting Methods #

RDoc for methods is most useful when the method has parameters, return values, or possible exceptions that aren’t obvious from the method name alone.

# Calculates the final product price after discount and tax.
#
# Calculation order: base_price → subtract discount → add tax.
# The discount is applied first before tax is computed.
#
# @param base_price [Float, Integer] Price before discount and tax.
#   Must be a positive value.
# @param discount [Float] Discount percentage as a decimal (0.0 - 1.0).
#   Example: 0.1 for a 10% discount.
# @param tax_rate [Float] Tax rate as a decimal.
#   Defaults to the 11% VAT (0.11).
#
# @return [Float] Final price after discount and tax, rounded
#   to two decimal places.
#
# @raise [ArgumentError] If base_price is negative or discount is outside
#   the 0.0 - 1.0 range.
#
# @example Normal price with standard VAT
#   calculate_price(100_000, 0.1)         # => 99_000.0
#
# @example Price with a custom tax rate
#   calculate_price(100_000, 0.2, 0.05)   # => 84_000.0
def calculate_price(base_price, discount, tax_rate = 0.11)
  raise ArgumentError, "Price cannot be negative" if base_price < 0
  raise ArgumentError, "Discount must be between 0 and 1" unless (0.0..1.0).include?(discount)

  after_discount = base_price * (1 - discount)
  after_tax      = after_discount * (1 + tax_rate)
  after_tax.round(2)
end

Documenting Constants and Attributes #

class ServerConfig
  # Maximum allowed database connections per instance.
  # This value is determined by the PostgreSQL connection pool capacity
  # and the number of Puma workers running concurrently.
  MAX_CONNECTIONS = 25

  # Request timeout in seconds before a connection is considered failed.
  # Values above 30 seconds aren't recommended because they will
  # significantly affect the user experience.
  TIMEOUT_SECONDS = 30

  # @return [String] Hostname or IP of the active database server.
  attr_reader :db_host

  # @return [Integer] Port used for the connection.
  #   Default: 5432 for PostgreSQL.
  attr_accessor :port
end

Generating RDoc Documentation #

Once you’ve written RDoc comments, you can generate HTML documentation with the following commands:

# Generate documentation for a single file:
rdoc lib/product.rb

# Generate documentation for the whole project:
rdoc lib/

# Generate with a richer format (darkfish template):
rdoc --format darkfish lib/

# Generate documentation and open it directly in the browser:
rdoc --format darkfish lib/ && open doc/index.html

The output is a doc/ folder containing HTML files you can open in a browser or host as a documentation site.


YARD — The Modern Ruby Documentation Standard #

Although RDoc is built into Ruby, the modern Ruby ecosystem mostly uses YARD (Yet Another Ruby Documentation). YARD is compatible with RDoc but offers richer syntax, more expressive type support, and better output.

# Install YARD:
gem install yard

# Generate documentation:
yard doc lib/

# Run a local documentation server:
yard server

RDoc vs YARD Syntax Comparison #

NeedRDocYARD
Parameter# @param [Type] name Description# @param name [Type] Description
Return value# @return [Type] Description# @return [Type] Description
Exception# @raise [Type] Description# @raise [Type] Description
Example# == Example# @example Title
Deprecatedmanual# @deprecated Message
Authormanual# @author Name
Versionmanual# @since version

Complete Documentation Example with YARD #

# Service for sending notifications across various channels (email, SMS, push).
#
# Every notification is sent asynchronously using a background job.
# Use {NotificationService#send_sync} if you need immediate confirmation.
#
# @example Send an email notification
#   svc = NotificationService.new
#   svc.send(user, :email, "Verify your account")
#
# @example Send to multiple channels at once
#   svc.send_multi(user, [:email, :sms], "Password reset successful")
#
# @since 1.2.0
# @author Platform Team
class NotificationService

  # Sends a notification to a single channel asynchronously.
  #
  # @param recipient [User] The user object receiving the notification.
  #   The user must have a verified email or phone number.
  # @param channel [Symbol] Delivery channel. Valid values:
  #   `:email`, `:sms`, `:push`, `:in_app`.
  # @param message [String] Notification content. Max 500 characters for SMS.
  # @param priority [Symbol] Delivery priority level.
  #   `:normal` (default) is processed within 5 minutes, `:high` within 30 seconds.
  #
  # @return [String] The background job ID that was created.
  #   Use this ID to track delivery status.
  #
  # @raise [ArgumentError] If the channel is invalid.
  # @raise [User::InactiveError] If the recipient's account is suspended.
  #
  # @example
  #   job_id = svc.send(user, :email, "Welcome!")
  #   puts job_id  # => "job_a1b2c3d4"
  def send(recipient, channel, message, priority: :normal)
    # implementation...
  end
end

Magic Comments in Ruby #

Besides regular comments and documentation, Ruby also recognizes magic comments — special comments on the first or second line of a file that change interpreter behavior.

# frozen_string_literal: true

# The magic comment above makes every string literal in this file
# frozen (immutable). This improves performance because Ruby
# doesn't need to allocate a new String object every time the
# same literal is encountered.

name = "Ruby"
name << " on Rails"  # => FrozenError: can't modify frozen String
# encoding: utf-8

# Explicitly sets the file encoding.
# Since Ruby 2.0, UTF-8 is the default, but this is useful
# when working with files that use a different encoding.
# warn_indent: true

# Enables warnings when code indentation is inconsistent.
# Useful when refactoring legacy code that might have
# indentation inconsistencies.
flowchart TD
    A[Comment Types in Ruby] --> B[Regular Comments]
    A --> C[Magic Comments]
    A --> D[Documentation]
    B --> B1["# single-line"]
    B --> B2["=begin...=end\nmulti-line"]
    B --> B3["Special tags\nTODO/FIXME/HACK"]
    C --> C1["frozen_string_literal: true"]
    C --> C2["encoding: utf-8"]
    C --> C3["warn_indent: true"]
    D --> D1["RDoc\nbuilt into Ruby"]
    D --> D2["YARD\nmodern standard"]

Comments in a Team Context #

Comments have a social dimension that’s often overlooked — they’re written communication you leave for other developers (or your future self). A few principles that hold up in any team:

Comments must stay in sync with the code. An inaccurate comment is more dangerous than no comment at all — it misleads. Every time you change code, check whether the surrounding comments are still relevant.

Code that needs lots of comments to understand is a refactoring signal. If you need to write a long paragraph to explain a method, consider whether that method could be split into several smaller methods with more descriptive names.

# ANTI-PATTERN: a complex method that needs lots of comments
def process(u, o, p)
  # u is user, o is order, p is payment
  # First check whether the user is active and the order is unpaid
  # then validate the chosen payment method
  # then deduct balance or charge the card
  # finally update the order status and send a confirmation email
  return false unless u.active? && o.unpaid?
  return false unless p.valid?
  p.process
  o.mark_paid
  UserMailer.payment_confirmation(u, o).deliver_later
  true
end

# CORRECT: a method whose name and structure are self-documenting
def complete_payment(user, order, payment)
  return false unless user_and_order_valid?(user, order)
  return false unless payment.valid?

  process_transaction(payment, order)
  send_confirmation(user, order)
  true
end

private

def user_and_order_valid?(user, order)
  user.active? && order.unpaid?
end

def process_transaction(payment, order)
  payment.process
  order.mark_paid
end

def send_confirmation(user, order)
  UserMailer.payment_confirmation(user, order).deliver_later
end

Summary #

  • Two comment syntaxes# for single-line (most common), =begin...=end for multi-line (limited to the top level of a file).
  • Stacked # comments are more flexible than =begin...=end — they can go anywhere, including inside methods and class bodies.
  • Explain why, not what — the code already explains what it does; the best comments explain the reasoning behind non-obvious design decisions.
  • TODO/FIXME/HACK tags are useful for marking unfinished work, but don’t let them pile up without follow-up.
  • Magic comments change interpreter behavior# frozen_string_literal: true is the most common and is recommended for performance.
  • RDoc is Ruby’s built-in documentation system — comments above classes and methods can be turned into HTML documentation with the rdoc command.
  • YARD is the modern documentation standard — more expressive than RDoc with @param, @return, @raise, @since, and more.
  • Out-of-sync comments are more dangerous than no comments — always update comments when you change the surrounding code.
  • Code that needs many comments is a refactoring signal — descriptive method and variable names dramatically reduce the need for comments.

← Previous: Core Syntax   Next: Variables →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact