Debugging is a crucial skill for any Ruby developer. And as the tools and techniques evolve, it’s important to keep up-to-date with the latest best practices.

So here are some of my Ruby debugging tips and recommendations that I’d offer to Ruby developers in 2025.

  • You can use the Ruby LSP extension to connect to debug.gem too. It requires a slightly different launch.json configuration (example) and provides better error handling for connection issues.
  • Try using launch request in launch.json instead of attach. It simplifies the debugging process as you don’t need to manually start/stop the server. In most Rails projects, a simple entry like this will do:

    {
      "version": "0.2.0",
      "configurations": [
        {
          "type": "ruby_lsp",
          "name": "Launch Server",
          "request": "launch",
          "program": "bin/rails s",
        },
      ]
    }
    
  • The effectiveness of your debugging session heavily depends on your ability to navigate between methods, classes, and files. Make sure you have a good editor setup, such as Ruby LSP.
  • Use Ruby LSP’s Code Lens feature to easily debug tests in both terminal and VS Code.
  • Use gem "debug", require: "debug/prelude" in your Gemfile instead.

    debug.gem is activated when required, which usually isn’t necessary when you’re not debugging.

    By requiring debug/prelude, it defines the breakpoint methods like breakpoint and binding.break, but doesn’t activate the gem immediately.

    (This has been the default in newly generated Rails projects since Rails 7.2.)

  • If you want to prevent debug.gem from being used in certain environments, you can set RUBY_DEBUG_ENABLE to 0.

    For example, setting this on CI will make debugger or binding.break raise an error rather than hang indefinitely.

  • You can configure debug.gem to ignore certain gems when debugging. For example:

      begin
        # Try to load debug, but only just the config component so we don't activate it by accident
        require "debug/config"
    
        zeitwerk_paths = Gem.loaded_specs["zeitwerk"].full_require_paths.freeze
        bootsnap_paths = Gem.loaded_specs["bootsnap"].full_require_paths.freeze
    
        DEBUGGER__::CONFIG[:skip_path] = Array(DEBUGGER__::CONFIG[:skip_path]) + zeitwerk_paths + bootsnap_paths
      rescue LoadError
        # In case debug.gem is not installed for any reason
        # Such as when this file being loaded from production where debug.gem is usually not installed
      end
    
    • If you use Sorbet, you can add sorbet-runtime to the ignore list too:

      sorbet_paths = Gem.loaded_specs["sorbet-runtime"].full_require_paths.freeze
      DEBUGGER__::CONFIG[:skip_path] = Array(DEBUGGER__::CONFIG[:skip_path]) + sorbet_paths
      
  • For most users, I recommend activating debug.gem’s integration with IRB for a better debugging experience. You can do this by:
    • Setting RUBY_DEBUG_IRB_CONSOLE to 1
    • Setting DEBUGGER__::CONFIG[:irb_console] to true
  • The following debug.gem commands will repeat when hitting enter:
    • step (s)
    • next (n)
    • continue (c)
    • finish (fin)
    • until (u)
    • up
    • down

    For example, if you type s + enter and then hit enter again, it’ll repeat the step command.

  • You can use debugger(do: "...") or debugger(pre: "...") to automatically execute a command after a breakpoint is hit:

      # This will print the local variables and open the console
      debugger(pre: "info locals")
      # This will print the local variables and continue the program
      debugger(do: "info locals")
    
  • The combination of trace exception and catch [exception] commands can make debugging control-flow related bugs easier:
    • trace exception will print traces when an exception is raised
    • catch [exception] will break when the exception is raised
  • Using the combination of bt [n] and up/down commands is often more effective than setting multiple breakpoints on the same code path.

  • For more fine-grained tracing, you can use the tracer gem.

  • debug.gem freezes all running threads when it enters a breakpoint. If this causes issues, use binding.irb as an alternative.

  • You can use binding.irb for light debugging to open a REPL, and then activate debug.gem with its debug command.
  • For learning more fundamental debugging concepts, I think my talk at RubyKaigi 2022 should still be helpful.

I hope you find these tips useful. Happy debugging!