Reason #148 • May 28th, 2026

Using blocks for callbacks

Since Ruby code is generally synchronous in nature, we don't have the same need for callbacks as we do in event-driven languages like JavaScript. However, it is still quite common to design callback-like APIs in Ruby and use blocks to implement them. Some examples include:

OptionParser's on method, which allows us to register blocks to be called when specific command-line options are encountered:

Ruby
require "optparse"

options = {}

OptionParser.new do |op|
  op.on("-n", "--name NAME", "Your name") do |name|
    options[:name] = name
  end

  op.on("-a", "--age AGE", "Your age") do |age|
    options[:age] = age.to_i
  end
end.parse!
    

RSpec's before, after and around hooks, which allow us to register blocks to be called before, after or around each example:

Ruby
RSpec.describe MyApp do
  before do
    # This block will be called before each example
    setup_test_data
  end

  after do
    # This block will be called after each example
    cleanup_test_data
  end

  around do |example|
    # This block will be called around each example
    MyApp.silence_logs do
      example.run
    end
  end

  it "does something" do
    # ...
  end

  it "does something else" do
    # ...
  end
end
    

The Glimmer DSL for building desktop applications, which uses blocks both to define the structure and input events of the UI:

Ruby
require "glimmer-dsl-libui"

include Glimmer

window("Callbacks", 320, 220) {
  vertical_box {
    horizontal_box {
      stretchy false

      @status = label("Move or type in the blue area")

      button("Click me") {
        stretchy false
        on_clicked { @status.text = "button clicked" }
      }
    }

    area {
      on_draw do |p|
        rectangle(0, 0, p[:area_width], p[:area_height]) {
          fill r: 225, g: 240, b: 255
        }
      end

      on_mouse_moved { |e| @status.text = "moved to #{e[:x]}, #{e[:y]}" }
      on_mouse_down { |e| @status.text = "mouse down: #{e[:button]}" }
      on_mouse_up { |e| @status.text = "mouse up: #{e[:button]}" }
      on_key_down { |e| @status.text = "key down: #{e[:key]}" }
      on_key_up { |e| @status.text = "key up: #{e[:key]}" }
    }
  }
}.show
    

Feel free to gem install glimmer-dsl-libui and run the above code to see it in action BTW!

Make your own

To implement this kind of API, we can receive the blocks as arguments via &block and store them in instance variables to be called later when the relevant event occurs. For example, we could implement a simple API allowing us to register before hooks like this:

Ruby
class TestSuite
  def initialize
    @before_hooks = []
  end

  def before(&block)
    @before_hooks << block
  end

  def run_example(example)
    @before_hooks.each(&:call)
    example.call
  end
end
    

Easy peasy!