Using blocks for custom control flow
Blocks can also be used to implement custom control flow patterns, where we take an existing piece of code, pass it as a block to method that controls when, whether or how that code is executed. An example from the Ruby standard library is Timeout.timeout, which executes a block of code but raises an exception if it takes longer than a specified amount of time to complete:
require "timeout"
Timeout.timeout(3) do
sleep(2)
"Finished sleeping!"
end
# => "Finished sleeping!"
Timeout.timeout(1) do
sleep(2)
end
# => raises Timeout::Error: execution expired
Disclaimer: You may want to avoid using Timeout.timeout unless you are in complete control of the code inside the block passed, as it can corrupt state and lead to unpredictable behavior. Better ways of handling timeouts are covered in The Ultimate Guide to Ruby Timeouts by Andrew Kane.
In any case, let's implement our own quite safe example - a with_retries method that takes a block and retries executing it a specified number of times if it encounters an exception:
def with_retries(max_retries)
retries = 0
begin
yield
rescue
retries += 1
if retries <= max_retries
puts "Retrying... (#{retries}/#{max_retries})"
retry
else
raise
end
end
end
with_retries(3) do
puts "Trying..."
raise "Something went wrong!"
end
# Output:
# Trying...
# Retrying... (1/3)
# Trying...
# Retrying... (2/3)
# Trying...
# Retrying... (3/3)
# Trying...
# raises: Something went wrong! (RuntimeError)
with_retries(10) do
puts "Trying..."
raise "Something went wrong!" if rand < 0.8
puts "Success!"
end
# Output will vary, but it will keep retrying until it either succeeds or reaches the max retries.
Pardons for describing this as "custom control flow", which might not be the most precise term (and could probably be applied to some of the previous block articles as well), but it felt like the best way to differentiate this use case from the others.
Happy Friday! ❤️