Kernel#trace_var
trace_var(symbol) {|new_value| block } -> symbol The trace_var method monitors changes to global variables. When the variable is modified, the provided block is called with the new value. This is useful for debugging and monitoring.
Basic Usage
# Trace a global variable
$counter = 0
trace_var(:$counter) do |value|
puts "Counter changed to: #{value}"
end
$counter = 5 # Prints: Counter changed to: 5
$counter = 10 # Prints: Counter changed to: 10
The block receives the new value as its argument, so you can inspect or react to the change immediately. The trace fires synchronously at the point of assignment, which means the callback runs before the next line of code executes. This tight coupling is useful for debugging but can also introduce performance overhead if the block does heavy work, so keep the callback lightweight.
Practical Examples
Debugging global state
$debug_mode = false
trace_var(:$debug_mode) do |value|
puts "Debug mode #{value ? 'enabled' : 'disabled'}"
$DEBUG = value
end
The debugging example shows how trace_var can mirror a global variable to the built-in $DEBUG flag. Whenever $debug_mode flips, the trace block updates $DEBUG accordingly and prints a status message. This is a small but useful pattern for toggling verbose output in development without scattering conditional print statements through the codebase.
Configuration Changes
$config = { log_level: "info" }
trace_var(:$config) do |new_config|
puts "Configuration changed: #{new_config}"
apply_config(new_config)
end
$config = { log_level: "debug" }
This example traces a configuration hash, which is a practical use of trace_var in applications that use global config objects. The callback applies the new configuration automatically, so any code that reassigns $config triggers a reconfiguration without needing to call a separate setup function. That keeps the configuration change and its side effects in one place.
Variable access logging
$cache = {}
trace_var(:$cache) do |value|
timestamp = Time.now
puts "[#{timestamp}] Cache updated: #{value.keys.size} entries"
end
The cache monitoring example logs a timestamp and the number of cache entries whenever $cache is reassigned. This is a lightweight way to track how often a shared data structure changes without adding logging calls throughout the application. The timestamp gives you a rough audit trail that can help diagnose stale-data bugs or unexpected cache flushes.
Breaking on Changes
$important_value = 100
trace_var(:$important_value) do |val|
if val < 0
puts "WARNING: Value went negative!"
# Could raise exception or log
end
end
This example adds a guard clause inside the trace block, which is a common pattern for validating changes. The callback fires on every assignment, so the block can inspect the new value and take action only when a specific condition is met. Calling trace_var without a block removes the trace, so the hook does not outlive its purpose.
Untrace
# Remove the trace
trace_var(:$counter) # Returns nil when no block given, or removes trace
The trace_var method only works with global variables, which limits its usefulness in well-factored Ruby programs that avoid globals. The built-in globals like $LOAD_PATH are an exception because they are part of the Ruby runtime itself and are safe to monitor. For application-level state, instance variables with custom setters are almost always the better choice.
Limitations
# Only works with global variables ($ prefix)
# local_var = 1
# trace_var(:local_var) # Won't work!
# Works with these built-in globals
trace_var(:$LOAD_PATH) do |path|
puts "Load path changed to: #{path}"
end
The trace_var method only works with global variables, which limits its usefulness in well-factored Ruby programs that avoid globals. The built-in globals like $LOAD_PATH are an exception because they are part of the Ruby runtime itself and are safe to monitor. For application-level state, instance variables with custom setters are almost always the better choice.
Comparison with attr_accessor
# For instance variables, use a different approach
class Config
def self.value
@value
end
def self.value=(val)
puts "Setting to #{val}"
@value = val
end
end
The comparison with attr_accessor shows that for instance variables, you need a different approach entirely. By defining custom setter methods, you can achieve the same kind of change notification without relying on the global variable tracing mechanism. This is the preferred pattern for object-oriented Ruby code because it keeps the tracing logic encapsulated within the class.
Use Cases
- Debugging global state changes
- Implementing reactive global variables
- Logging configuration changes
- Memory profiling (tracking allocations)
using trace hooks carefully
trace_var can be useful when you need to see exactly when a global changes, but it should stay in a narrow debugging or tooling role. Because the callback runs every time the variable changes, the block should be short and predictable. Heavy work in the hook can hide the real behavior you are trying to study. A small log line or a single guard is usually enough to make the change visible without turning the hook into its own subsystem.
It is also worth thinking about how long the trace should stay active. In a test or troubleshooting session, the trace can live just long enough to gather the information you need. In a larger program, removing the trace when you are done keeps the global state easy to reason about and prevents unrelated code from being surprised by the extra callback.
keeping the hook temporary
A tracing hook should answer a narrow question and then go away. That usually means using it for a short debugging session, a focused test, or a small tool that watches one value at a time. The more the hook starts to do, the harder it becomes to reason about the real program flow. Keeping the trace temporary also helps the rest of the code stay clean, because the debugging support does not linger after the issue is solved.