skip to content

In Ruby, what happens when execution reaches binding.irb, and how do you resume or stop the program afterwards?

level: juniorimportance: must knowfreq 68%

answer

  1. a prompt opened inside a live scope
  2. locals, self and ivars are real
  3. whereami (alias @) reprints the code
  4. exit resumes, exit! ends the process
  5. disable_irb for a breakpoint in a loop

basics

~20 s

binding.irb pauses the program at that line and opens an IRB prompt inside that scope, with its locals, self and instance variables live. exit (or Ctrl-D) resumes the program from that line; exit! ends the process.

solid answer

~40 s

`binding` returns the current execution context and `Binding#irb` starts an IRB session on it, so the program stops at that line and prints the surrounding code. At the prompt you can read and even reassign locals, call private methods of `self`, and inspect instance variables, because you are evaluating inside that scope, not in a copy. `whereami` (alias `@`) shows the code again. `exit` (or Ctrl-D) leaves the session and the program **continues** from the line after `binding.irb`; `exit!` ends the whole process through `Kernel.exit`. If the breakpoint sits in a loop, `disable_irb` turns every later `binding.irb` into a no-op for the rest of the run. Ctrl-C does not leave the session by default.

code

ruby · 21 lines
ruby
require "csv"

class CsvImporter
  def initialize
    @errors = []
  end

  def import(text)
    CSV.parse(text, headers: true).each_with_index do |row, i|
      binding.irb if row["email"].to_s.empty?
      @errors << i unless valid?(row)
    end
    @errors
  end

  private

  def valid?(row) = row["email"].to_s.include?("@")
end

CsvImporter.new.import("name,email\nAda,[email protected]\nBob,\n")

go deeper

for a junior

Recall that binding.irb opens a prompt in the live scope, that exit resumes the program, and that exit! ends it. Show you can read locals and instance variables there.

for a middle

Explain that the prompt evaluates inside the captured Binding, so assignments and side effects are real, and use conditional breakpoints or disable_irb for loops.

for a senior

Talk about keeping breakpoints out of committed code, how a stray prompt hangs a server or CI job, and when to escalate from binding.irb to IRB's debug command.

for a principal

Weigh ad hoc breakpoints against logging and reproducible tests as a team habit: which debugging workflows you standardise, and how you keep console access out of production paths.

## What binding.irb actually is Ruby's `Kernel#binding` returns a **`Binding`**: an object that captures an execution context at one point in a program — its local variables, the value of `self`, and the method and block it belongs to. IRB, Ruby's interactive console, adds a method to that object: **`Binding#irb`**. Calling `binding.irb` in the middle of your code therefore means "open an IRB prompt that evaluates everything I type *inside this exact context*". In Ruby 4.0 IRB is a **bundled gem** rather than part of the standard library, but you still do not need a `require` line: Ruby's prelude defines a small `Binding#irb` that requires `irb` on first use, and if a plain `require` fails under Bundler it force-activates the installed bundled copy. Listing `gem "irb"` in the Gemfile's development group is still the tidy choice for a project. ## What you see when it fires When execution reaches the line, IRB: 1. prints `From: file.rb @ line N` and a few lines of code around the call, with an arrow on the current line (pass `binding.irb(show_code: false)` to skip this); 2. opens a prompt whose label shows the current `self`, for example `irb(#<CsvImporter:0x...>):001>`; 3. waits: the rest of the method has **not** run yet. At the prompt you are not looking at a snapshot. You can: - read locals such as `row` or `line_no` by name; - **assign** to them, and the program sees the new value when it resumes; - read `@instance_variables` of `self` and call its **private** methods without a receiver; - call any code the program could call at that point, including methods with side effects (a database write is a real write). `whereami` (alias `@`) reprints the surrounding code after your own output has scrolled it away. ## Leaving the session | You type | What happens | |---|---| | `exit` or Ctrl-D | The IRB session ends and the program **continues** from the line after `binding.irb`. | | `exit!` | IRB calls `Kernel.exit`: a `SystemExit` ends the process, so `ensure` blocks and `at_exit` hooks still run. | | `disable_irb` | Redefines `Binding#irb` as an empty method for the rest of the process, then leaves the session and continues. | | Ctrl-C | Interrupts the current input only; `IRB.conf[:IGNORE_SIGINT]` is `true` by default, so the session stays open. | The most common surprise is `exit`: newcomers expect it to stop the program, as it would in a plain script, and are surprised when the program carries on and perhaps hits the breakpoint again. ## Breakpoints in loops and hot paths A `binding.irb` inside `each` stops on **every** iteration. Three ways out: - make it conditional: `binding.irb if row["email"].nil?` stops only on the row you care about; - type `disable_irb` once you have seen enough, so later hits are skipped; - type `exit!` to end the run altogether. A conditional breakpoint is usually the right first move: when a CSV import fails on row 4,812, you want the prompt on that row, not on the first one. ## A worked example Suppose `CsvImporter#import_row` is rejecting a row and you do not know why. Put `binding.irb if row["email"].to_s.empty?` inside the method and run the import. When the prompt opens you can type `row` to see the parsed fields, `row.to_h` to see headers and values together, `@errors` to see what the object has collected so far, and call a private helper like `normalize(row)` to watch it return. Fix a local (`row["email"] = "[email protected]"`), type `exit`, and the method carries on with the corrected value — a quick way to confirm a hypothesis before editing the file. ## Good habits - Treat `binding.irb` as temporary: remove it before committing, because a forgotten one can leave a server or a CI job waiting for console input. - Prefer a conditional breakpoint to a bare one inside loops. - Remember that anything you call runs for real; inspect before you mutate. - When you need to step line by line from the prompt, IRB's `debug` command hands the same session to the debug gem.

  • If you assign to a local variable at the binding.irb prompt, does the running method see the change after exit?
    Yes. The prompt evaluates inside the captured binding, not a copy, so assigning `row = other_row` or mutating `row["email"]` changes what the method uses when it resumes. That is what makes `binding.irb` useful for testing a fix in place — and why side-effecting calls at the prompt are real.
  • Your binding.irb sits inside a loop over 10,000 rows. How do you avoid typing exit ten thousand times?
    Make the breakpoint conditional (`binding.irb if row["email"].nil?`) so it fires only for the row you care about, or type `disable_irb` at the prompt, which redefines `Binding#irb` as a no-op for the rest of the process and lets the run finish. `exit!` ends the process instead.
  • Does exit! at an IRB prompt skip ensure blocks and at_exit hooks like Kernel#exit! does?
    No. IRB's `exit!` command leaves the session and then calls `Kernel.exit`, which raises `SystemExit`, so `ensure` clauses and `at_exit` hooks still run. It is not the same as calling `Kernel#exit!` in your own code, which terminates immediately without them.

binding.irb is like pausing a film and stepping into the scene: you can pick up the props and move them, and when you press play the story continues with whatever you changed.

saying these in an interview costs you the question

  • Typing exit at the binding.irb prompt stops the whole program.
  • The prompt works on a copy, so assigning a local changes nothing.
  • Private methods of self cannot be called from the binding.irb prompt.
  • Ctrl-C is the normal way to leave a binding.irb session.
  • binding.irb needs require "irb" at the top of every file.