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.
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",
},
]
}
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
RUBY_DEBUG_IRB_CONSOLE
to 1
DEBUGGER__::CONFIG[:irb_console]
to true
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")
trace exception
and catch [exception]
commands can make debugging control-flow related bugs easier:
trace exception
will print traces when an exception is raisedcatch [exception]
will break when the exception is raisedUsing 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.
binding.irb
for light debugging to open a REPL, and then activate debug.gem with its debug
command.I hope you find these tips useful. Happy debugging!