Control Flow
Intuition
Section titled “Intuition”Ruby’s control flow is expressive and permissive. Conditionals can be used as expressions that return values, and modifiers let you append conditions to single statements. The case statement performs pattern matching against ranges, regexes, and objects. Loops in Ruby are actually method calls with blocks, meaning iterators like each and times are the idiomatic way to repeat operations. Exceptions provide structured error handling, and blocks create scopes that manage resources like file handles automatically.
Conditionals
Section titled “Conditionals”if / elsif / else
Section titled “if / elsif / else”The if statement evaluates a condition and executes the corresponding branch. The elsif keyword handles additional conditions, and else provides a fallback:
score = 85
if score >= 90 puts "Grade: A"elsif score >= 80 puts "Grade: B"elsif score >= 70 puts "Grade: C"else puts "Grade: F"end## => "Grade: B"
## if as a modifier (postfix)puts "Pass" if score >= 60puts "Fail" if score < 60
# if as an expression (returns the last evaluated value)grade = if score >= 90 "A" elsif score >= 80 "B" elsif score >= 70 "C" else "F" endputs grade # => "B"
# Nested ifif user if user.active? if user.premium? puts "Premium active user" end endendunless
Section titled “unless”unless is the negation of if. It executes the body when the condition is falsy:
user = nil
unless user puts "No user found"end# => "No user found"
# unless with elseunless score >= 60 puts "Failed"else puts "Passed"end
# unless as modifierputs "No user" unless user
# Avoid double negatives -- prefer if for complex negations# Badunless !user.active? deactivate(user)end
# Goodif user.active? deactivate(user)endTernary Operator
Section titled “Ternary Operator”The ternary operator is a concise inline conditional:
status = score >= 60 ? "Pass" : "Fail"
# Equivalent tostatus = if score >= 60 "Pass" else "Fail" end
# Nested ternaries (avoid when possible)label = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F"
# Use case/when instead for multiple conditionscase / when
Section titled “case / when”The case statement is Ruby”s switch construct. It supports both value matching and condition matching:
# Value matching (=== operator)action = "delete"
case actionwhen "create" puts "Creating resource"when "read" puts "Reading resource"when "update" puts "Updating resource"when "delete" puts "Deleting resource"else puts "Unknown action"end
# Case with expressionscase scorewhen 90..100 then "A"when 80...90 then "B"when 70...80 then "C"when 0...70 then "F"else "Invalid"end
# Case with multiple valuesrole = "admin"
case rolewhen "admin", "superadmin" puts "Full access"when "editor" puts "Edit access"when "viewer" puts "Read-only access"end
# Case with regexcase commandwhen /^create/ puts "Create command"when /^delete/ puts "Delete command"when /^update/ puts "Update command"else puts "Unknown command"end
# Case with procs/lambdas (Ruby 2.7+ pattern matching alternative)even = -> (n) { n.even? }odd = -> (n) { n.odd? }
case 4when even puts "Even number"when odd puts "Odd number"end
# Case as expressioncategory = case score when 90..100 then "excellent" when 80...90 then "good" when 70...80 then "average" else "below average" end
# Case with no argument (acts like a series of if/elsif)number = 42
casewhen number < 0 puts "Negative"when number == 0 puts "Zero"when number > 0 puts "Positive"endPattern Matching (Ruby 2.7+, stable in 3.0+)
Section titled “Pattern Matching (Ruby 2.7+, stable in 3.0+)”# case/in with pattern matchingcase { status: 200, body: "OK" }in { status: 200..299, body: } puts "Success: #{body}"in { status: 400..499 } puts "Client error"in { status: 500..599 } puts "Server error"end
# Array pattern matchingcase [1, 2, 3]in [Integer, Integer] puts "Two integers"in [Integer, Integer, Integer] puts "Three integers"end
# One-line pattern matching with =>{ status: 404 } => { status: }puts status # => 404
# Guard clauses with if in patternscase [1, 2, 3]in [a, b, c] if a == 1 puts "First element is 1"endThe while loop executes as long as the condition is truthy:
count = 5while count > 0 puts "Count: #{count}" count -= 1end# => Count: 5, 4, 3, 2, 1
# while as modifiercount = 5count -= 1 while count > 0
# Infinite loop with breakwhile true line = gets.chomp break if line == "quit" puts "You typed: #{line}"end
# Common pattern: reading inputwhile (line = gets) break if line.strip.empty? process(line)enduntil is the inverse of while — it executes while the condition is falsy:
count = 0until count >= 5 puts count count += 1end# => 0, 1, 2, 3, 4
# until as modifiercount = 0count += 1 until count >= 5The for loop iterates over collections:
# for over arraysfor element in [10, 20, 30] puts elementend
# for over rangesfor i in 1..5 puts iend
# for over hashesfor key, value in { a: 1, b: 2, c: 3 } puts "#{key}: #{value}"end
# Note: for does NOT create a new scope -- the loop variable leaksfor i in 1..3 # i is accessible after the loopendputs i # => 3
# Prefer each over for -- it creates a proper block scope[1, 2, 3].each do |i| # i is local to this blockend# i is not accessible here (nil in Ruby 3.0+)The loop method creates an infinite loop, combined with break:
loop do print "Enter command (q to quit): " input = gets.chomp break if input == "q" process_command(input)end
# loop with a countercount = 0loop do count += 1 break if count >= 10end
# loop returns the value passed to breakresult = loop do break 42 if some_conditionend# result => 42Iterators
Section titled “Iterators”Ruby emphasises iteration over manual loops. Iterators are methods that yield values to a block:
The most fundamental iterator — yields each element of a collection:
# Array each[1, 2, 3].each do |element| puts elementend
# Hash each{ a: 1, b: 2 }.each do |key, value| puts "#{key}: #{value}"end
# String each_char"hello".each_char do |char| puts charend
# Range each(1..5).each { |i| puts i }
# each_line for strings"line1\nline2\nline3".each_line do |line| puts line.chompend
# each_with_index["a", "b", "c"].each_with_index do |element, index| puts "#{index}: #{element}"end# => 0: a, 1: b, 2: c5.times do |i| puts "Iteration #{i}"end# => Iteration 0, 1, 2, 3, 4
5.times { puts "Hello" } # prints "Hello" 5 times
# times returns self (the integer)result = 3.times.map { |i| i * 2 } # => [0, 2, 4]upto and downto
Section titled “upto and downto”1.upto(5) { |i| puts i }# => 1, 2, 3, 4, 5
5.downto(1) { |i| puts i }# => 5, 4, 3, 2, 1
# With step1.step(10, 2) { |i| puts i }# => 1, 3, 5, 7, 9
# upto with string"a".upto("e") { |c| puts c }# => a, b, c, d, eeach_with_index and with_index
Section titled “each_with_index and with_index”["apple", "banana", "cherry"].each_with_index do |fruit, index| puts "#{index}. #{fruit}"end
# with_index on any iterator that yields a single value(10..20).each.with_index do |number, index| puts "#{index}: #{number}"end
# with_index with an offset["a", "b", "c"].each.with_index(1) do |letter, index| puts "#{index}. #{letter}"end# => 1. a, 2. b, 3. cHigher-Order Iterators
Section titled “Higher-Order Iterators”# map/collect -- transform each element[1, 2, 3].map { |n| n ** 2 }# => [1, 4, 9]
# select/reject -- filter elements[1, 2, 3, 4, 5].select { |n| n.even? }# => [2, 4]
[1, 2, 3, 4, 5].reject { |n| n.even? }# => [1, 3, 5]
# reduce/inject -- accumulate[1, 2, 3, 4, 5].reduce(0) { |sum, n| sum + n }# => 15
[1, 2, 3, 4, 5].reduce(1, :*)# => 120 (product using symbol shorthand)
# group_by -- group by key["alice", "bob", "charlie"].group_by { |name| name.length }# => {5=>["alice"], 3=>["bob"], 7=>["charlie"]}
# sort_by -- sort by key["banana", "apple", "cherry"].sort_by { |word| word.length }# => ["apple", "banana", "cherry"]
# partition -- split into two arrays[1, 2, 3, 4, 5].partition { |n| n.even? }# => [[2, 4], [1, 3, 5]]
# each_slice -- iterate in groups(1..10).each_slice(3).to_a# => [[1,2,3], [4,5,6], [7,8,9], [10]]
# each_cons -- iterate over consecutive groups[1, 2, 3, 4].each_cons(2).to_a# => [[1,2], [2,3], [3,4]]
# cycle -- repeat endlessly[:a, :b].cycle.take(6)# => [:a, :b, :a, :b, :a, :b]
# zip -- combine multiple arrays[1, 2, 3].zip([4, 5, 6])# => [[1,4], [2,5], [3,6]]
# flat_map/collect_concat -- flatten mapped results[[1, 2], [3, 4]].flat_map { |arr| arr.map { |n| n * 10 } }# => [10, 20, 30, 40]
# all?/any?/none?/one?[1, 2, 3].all? { |n| n > 0 } # => true[1, 2, 3].any? { |n| n > 5 } # => false[1, 2, 3].none? { |n| n > 5 } # => true[1, 2, 3].one? { |n| n == 2 } # => true
# count with block[1, 2, 3, 4, 5].count { |n| n.even? }# => 2
# find/detect -- first matching element[1, 2, 3, 4, 5].find { |n| n > 3 }# => 4
# max_by/min_by%w[apple banana cherry].max_by(&:length)# => "banana"
%w[apple banana cherry].min_by(&:length)# => "apple"Chaining Iterators
Section titled “Chaining Iterators”# Multiple transformations chainedresult = (1..100) .select { |n| n.even? } .map { |n| n ** 2 } .select { |n| n > 100 && n < 1000 } .sort .first(5)# => [144, 196, 256, 324, 400]
# Lazy evaluation for large or infinite collections# Avoids creating intermediate arraysresult = (1..Float::INFINITY) .lazy .select { |n| n.even? } .map { |n| n ** 2 } .select { |n| n < 1000 } .take(5) .to_a# => [4, 16, 36, 64, 100]
# Lazy avoids processing all elementshuge_range = (1..1_000_000) .lazy .select { |n| n % 17 == 0 } .map { |n| n * 2 } .first(3) .to_a# => [34, 68, 102]Loop Control: next, break, redo
Section titled “Loop Control: next, break, redo”Skips to the next iteration of the loop:
# Skip odd numbers(1..10).each do |i| next if i.odd? puts iend# => 2, 4, 6, 8, 10
# next with a value (returns the value instead of the block result)result = [1, 2, 3, 4, 5].map do |n| next nil if n.odd? # returns nil for odd numbers n * 2 # returns doubled value for evenend# => [nil, 4, nil, 8, nil]
# Skip to next with inline form[1, 2, 3, 4, 5].each { |i| next if i < 3; puts i }# => 3, 4, 5Exits the loop entirely:
# Find first element matching a conditionresult = [1, 2, 3, 4, 5, 6, 7].find do |n| break n if n > 4end# result => 5
# Break returns a value from the entire method callfound = loop do x = rand(1..100) break x if x > 90end# found => some number > 90
# break with a value in mapresult = [1, 2, 3, 4, 5].map do |n| break :done if n == 3 n * 2end# result => :done (break exits map entirely)Restarts the current iteration without re-evaluating the condition:
# Retry input until validvalid_input = nil
until valid_input print "Enter a number: " input = gets.chomp
if input.match?(/\A\d+\z/) valid_input = input.to_i else puts "Invalid input. Try again." endend
# redo in loopscount = 0results = []
3.times do value = rand(10) if value < 3 redo # retry this iteration else results << value count += 1 endend# Results will have 3 values all >= 3retry (exception handling)
Section titled “retry (exception handling)”retry restarts the begin block from the beginning. Use with caution to avoid infinite loops:
# Retry with a counterattempts = 0max_attempts = 3
begin attempts += 1 connect_to_databaserescue ConnectionError if attempts < max_attempts sleep(2 ** attempts) # exponential backoff retry else raise "Failed after #{max_attempts} attempts" endend
# retry restarts from begin# Use sparingly -- prefer explicit retry loopsdef fetch_with_retry(url, max_retries: 3) retries = 0 begin response = HTTP.get(url) raise TimeoutError if response.status == 408 response rescue TimeoutError, SocketError retries += 1 if retries <= max_retries sleep(2 ** retries) retry else raise end endendGuard Clauses
Section titled “Guard Clauses”Guard clauses improve readability by handling edge cases early:
# Without guard clauses (arrow anti-pattern)def process_order(order) if order if order.paid? if order.items.any? ship(order) else raise "Empty order" end else raise "Unpaid order" end else raise "No order" endend
# With guard clausesdef process_order(order) raise "No order" unless order raise "Unpaid order" unless order.paid? raise "Empty order" unless order.items.any?
ship(order)end
# Another exampledef calculate_discount(user, order) return 0 unless user
base = order.total > 100 ? 0.1 : 0.0 return base unless user.member?
base + 0.05endException Handling
Section titled “Exception Handling”begin / rescue / ensure / else
Section titled “begin / rescue / ensure / else”Ruby uses begin/rescue blocks for exception handling:
# Basic structurebegin # Code that might raise an exception result = 10 / 0rescue ZeroDivisionError # Handle the specific exception puts "Cannot divide by zero"end
# Full structurebegin file = File.open("data.txt") content = file.read result = process(content)rescue Errno::ENOENT puts "File not found"rescue IOError => e puts "I/O error: #{e.message}"rescue => e # Catch-all (only rescue StandardError subclasses) puts "Unexpected error: #{e.message}" puts e.backtrace.first(5)else # Executes only if no exception was raised puts "Processed successfully: #{result}"ensure # Always executes (like finally in Java) file&.closeendException Hierarchy
Section titled “Exception Hierarchy”Ruby exceptions form a class hierarchy. Exception is the root, but you should only rescue StandardError (or its subclasses) in application code:
Exception├── SystemExit # exit(), exit!├── SignalException # signals (SIGINT, SIGTERM)├── NoMemoryError # out of memory├── ScriptError│ ├── LoadError│ ├── NotImplementedError│ └── SyntaxError├── SecurityError├── StandardError # <-- rescue this in app code│ ├── ArgumentError│ ├── IOError│ │ ├── EOFError│ │ └── Errno::* (file system errors)│ ├── IndexError│ ├── KeyError # Ruby 2.5+│ ├── LocalJumpError│ ├── NameError│ ├── RangeError│ ├── RegexpError│ ├── RuntimeError│ ├── StopIteration│ ├── SyntaxError (in StandardError branch too)│ ├── ThreadError│ ├── TypeError│ └── ZeroDivisionError└── SystemStackError# Rescue specific exceptionsbegin JSON.parse(invalid_json)rescue JSON::ParserError => e puts "Invalid JSON: #{e.message}"end
begin File.read("/nonexistent/file.txt")rescue Errno::ENOENT => e puts "File not found: #{e.message}"rescue Errno::EACCES => e puts "Permission denied: #{e.message}"end
begin array[100]rescue IndexError => e puts "Index out of bounds: #{e.message}"endThe raise method creates and raises exceptions:
# Raise with a string messageraise "Something went wrong"
# Raise a specific exception classraise ArgumentError, "Invalid argument"
# Raise with a formatted messageraise ArgumentError, "Expected #{expected}, got #{actual}"
# Raise an existing exception objecterror = RuntimeError.new("Custom error")raise error
# Re-raise the current exception (in a rescue block)begin risky_operationrescue => e # do some logging raise # re-raises the same exception with original backtraceend
# raise with ensurebegin raise "Initial error"rescue => e puts "Caught: #{e.message}" raise RuntimeError, "Wrapped: #{e.message}"endCustom Exceptions
Section titled “Custom Exceptions”Define custom exception classes by inheriting from StandardError:
# Base custom exceptionclass ApplicationError < StandardError; end
# Specific exceptionsclass ValidationError < ApplicationError attr_reader :field, :value
def initialize(message = "Validation failed", field: nil, value: nil) @field = field @value = value super(message) endend
class NotFoundError < ApplicationError attr_reader :resource, :id
def initialize(message = "Resource not found", resource: nil, id: nil) @resource = resource @id = id super(message) endend
class AuthenticationError < ApplicationError; endclass AuthorizationError < ApplicationError; endclass PaymentError < ApplicationError; end
# Using custom exceptionsclass UserService def find_user!(id) user = User.find_by(id: id) raise NotFoundError.new("User not found", resource: "User", id: id) unless user user end
def update_email!(user, new_email) raise ValidationError.new("Invalid email format", field: "email", value: new_email) unless new_email.match?(/\A[^@\s]+@[^@\s]+\z/)
user.update!(email: new_email) end
def authenticate!(username, password) user = User.find_by(username: username) raise NotFoundError, "User "#{username}' not found" unless user raise AuthenticationError, "Invalid password" unless user.authenticate(password) raise AuthorizationError, "Account suspended" unless user.active? user endend
# Rescue custom exceptionsbegin user = UserService.new.authenticate!("alice", "wrong")rescue AuthenticationError => e puts "Auth failed: #{e.message}"rescue NotFoundError => e puts "Not found: #{e.message}"rescue ApplicationError => e puts "App error: #{e.class}: #{e.message}"endException Object Methods
Section titled “Exception Object Methods”begin raise ArgumentError, "Invalid value: nil"rescue => e e.class # => ArgumentError e.message # => "Invalid value: nil" e.backtrace # => Array of call stack strings e.backtrace[0] # => first line of backtrace e.backtrace.first(3) # => first 3 lines e.cause # => the exception that caused this one (Ruby 2.5+) e.inspect # => "#<ArgumentError: Invalid value: nil>" e.to_s # => "Invalid value: nil"
# Exception wrapping begin raise "original error" rescue => original raise StandardError, "wrapped: #{original.message}" endrescue StandardError => e puts e.cause # => nil (no cause tracking unless using raise cause: option)end
# Ruby 3.1+ cause trackingbegin raise "inner"rescue => inner raise "outer"rescue => outer outer.cause # => #<RuntimeError: inner>endretry with Exceptions
Section titled “retry with Exceptions”# Retry with exponential backoffclass ResilientClient def initialize(max_retries: 3, base_delay: 1) @max_retries = max_retries @base_delay = base_delay end
def get(url) retries = 0 begin response = HTTP.get(url) unless response.status == 200 raise RuntimeError, "HTTP #{response.status}" end response rescue SocketError, TimeoutError, RuntimeError => e retries += 1 if retries <= @max_retries delay = @base_delay * (2 ** (retries - 1)) sleep(delay) retry else raise end end endendRescue in Method Definitions
Section titled “Rescue in Method Definitions”Ruby allows a compact rescue syntax directly in method definitions:
# Instead of wrapping entire method in begin/rescuedef fetch_data(url) response = HTTP.get(url) JSON.parse(response.body)rescue JSON::ParserError {}rescue SocketError, TimeoutError => e { error: e.message }end
# Equivalent todef fetch_data(url) begin response = HTTP.get(url) JSON.parse(response.body) rescue JSON::ParserError {} rescue SocketError, TimeoutError => e { error: e.message } endend
# Multiple rescue clausesdef process(input) validate(input) transform(input) save(input)rescue ValidationError => e { status: 422, error: e.message }rescue SaveError => e { status: 500, error: e.message }rescue => e { status: 500, error: "Unexpected: #{e.message}" }endEnsure for Cleanup
Section titled “Ensure for Cleanup”# File handling with ensuredef read_config(path) file = nil begin file = File.open(path, "r") file.read ensure file&.close endend
# Database transaction with ensuredef with_transaction connection = DatabasePool.checkout begin connection.begin_transaction yield(connection) connection.commit rescue => e connection.rollback raise ensure DatabasePool.checkin(connection) endend
# Lock management with ensuredef with_lock(resource) lock = acquire_lock(resource) begin yield ensure release_lock(lock) endend
# Ensure always executesdef example begin raise "error" rescue puts "rescued" ensure puts "ensure runs" # always executes end # Output: "rescued", then "ensure runs"endCommon Control Flow Patterns
Section titled “Common Control Flow Patterns”Early Return
Section titled “Early Return”# Clear, linear flow with early returnsdef process_payment(order, user) return { error: "Order required" } unless order return { error: "User required" } unless user
unless order.items.any? return { error: "Empty order" } end
if order.total > user.credit_limit return { error: "Exceeds credit limit" } end
charge = PaymentGateway.charge(user.payment_method, order.total) return { error: charge.error } unless charge.success?
{ success: true, charge_id: charge.id }endFlattening Nested Conditions
Section titled “Flattening Nested Conditions”# Before: deeply nesteddef handle_request(request) if request.authenticated? if request.authorised? if request.valid? process(request) else respond_with_error(400, "Invalid request") end else respond_with_error(403, "Not authorised") end else respond_with_error(401, "Not authenticated") endend
# After: flattened with guardsdef handle_request(request) return respond_with_error(401, "Not authenticated") unless request.authenticated? return respond_with_error(403, "Not authorised") unless request.authorised? return respond_with_error(400, "Invalid request") unless request.valid?
process(request)endConditional Assignment
Section titled “Conditional Assignment”# ||= for default valuesname = nilname ||= "Anonymous"puts name # => "Anonymous"
# Only assigns if variable is nil or falseconfig = {}config[:timeout] ||= 30config[:timeout] ||= 60 # still 30
# &&= for conditional updatedebug = truedebug &&= false # debug => falsedebug &&= false # debug is still false, no change
# Ternary for computed defaultstimeout = ENV.fetch("TIMEOUT", 30).to_imode = ENV.key?("DEBUG") ? :debug : :productionCross-References
Section titled “Cross-References”- Variables and Types defines the data types that control flow statements operate on in conditional expressions.
- Methods and Blocks shows how control flow interacts with method definitions, blocks, and iteration patterns.
- Object-Oriented Programming applies control flow within class methods and object behaviour.
Common Mistakes
Section titled “Common Mistakes”Using = instead of == in conditionals: = is assignment, not comparison. Ruby allows this without error, causing silent bugs. Always use == for comparison in if and unless.
Confusing unless with if !: unless with an else clause reads confusingly and is error-prone. Avoid unless...else; use if with the negated condition instead.
Assuming && and || return booleans: These operators return the actual value of the last evaluated expression, not necessarily true or false. Use !! to force boolean conversion when needed.