rubyguides

Kernel#trap

trap(signal, handler) -> handler

The trap method registers a handler for operating system signals. Signals are notifications sent to your process by the OS or other processes, commonly used for graceful shutdown and handling interrupts.

Common signals

# INT - Interrupt (Ctrl+C)
trap(:INT) { puts "Interrupted!" }

# TERM - Termination request
trap(:TERM) { puts "Shutting down..." }

# KILL - Immediate termination (cannot trap)
# trap(:KILL) { }  # Doesn't work!

# HUP - Hangup (often used for config reload)
trap(:HUP) { load_config }

# USR1, USR2 - User-defined signals
trap(:USR1) { do_custom_action }

Signals can be specified as symbols or strings, and the handler runs when the OS delivers that signal to your process. Note that KILL and STOP cannot be trapped: the OS enforces their behavior regardless of what your Ruby code registers.

Handler types

Block handlers

# Block as handler
trap(:INT) do
  puts "\nStopping gracefully..."
  exit 0
end

A block handler is the most common form and gives you full control over the response. The block receives no arguments because Unix signals carry no payload beyond the signal number itself, so the handler must rely on shared state or global variables.

String handlers

# Built-in handler names
trap(:INT, "IGNORE")  # Ignore the signal
trap(:INT, "DEFAULT") # Default behavior
trap(:INT, "EXIT")    # Exit immediately

String handlers provide convenient shortcuts for common behaviors. "IGNORE" tells the process to disregard the signal entirely, "DEFAULT" restores the original system behavior, and "EXIT" terminates the process immediately without running any at_exit hooks. These shorthand forms cover the most frequent cases.

Method handlers

def handle_signal
  cleanup
  exit
end

trap(:INT, method(:handle_signal))

Passing a method object lets you reuse an existing method as a signal handler without wrapping it in an extra block. This is useful when the handler logic is already defined as a named method elsewhere in the class or module.

Practical examples

Graceful shutdown

shutdown_requested = false

trap(:INT) { shutdown_requested = true }
trap(:TERM) { shutdown_requested = true }

loop do
  break if shutdown_requested
  process_requests
  sleep 1
end

puts "Graceful shutdown complete"

The flag-based pattern keeps the signal handler minimal: it sets a boolean and returns, while the main loop checks the flag on each iteration. This avoids doing complex work inside the handler itself, which runs on the main thread.

Config reload

trap(:HUP) do
  puts "Reloading configuration..."
  load_config
  puts "Configuration reloaded"
end

The HUP signal is conventionally used for configuration reloads in Unix daemons. Trapping HUP and re-reading config files lets you change settings without restarting the entire process, making it a very common pattern in long-running services and web servers.

Signal-based communication

# Parent sends signal to child
child_pid = fork do
  trap(:USR1) { puts "Got signal!"; exit }
  sleep
end

Process.kill(:USR1, child_pid)
Process.wait(child_pid)

Parent and child processes can communicate through signals even though signals carry no data payload. The parent sends a signal as a trigger, and the child’s handler performs a predefined action. This pattern is common in process supervision, job coordination, and inter-process notification systems.

Ignoring signals

# Ignore SIGCHLD (prevent zombie processes)
trap(:CHLD, "IGNORE")

# Or for specific operations
trap(:INT, "IGNORE")  # Ctrl+C does nothing

Ignoring SIGCHLD prevents zombie processes by telling the kernel to reap child processes automatically. Ignoring INT makes the process immune to Ctrl+C, which is occasionally useful in critical sections that absolutely must not be interrupted mid-operation by a user.

Process groups

# Send to process group
Process.kill(:TERM, 0)  # Signal all processes in group

Signaling process group 0 targets every process in the caller’s process group, which is a convenient way to terminate a group of related processes at once. This approach is used in job control systems and process supervisors that manage multiple children.

Important notes

# KILL and STOP cannot be trapped
trap(:KILL, "IGNORE")  # Still kills the process!

The operating system enforces KILL and STOP at the kernel level, so no amount of trapping can prevent them. This is by design: these signals exist as a last resort to terminate or pause a process that has stopped responding to all other signals.

Ruby signal names

# Use symbols or strings
trap(:INT) {}
trap("INT") {}
trap(Signal.list["INT"]) {}

Signal.list returns a hash mapping signal name strings to their numeric values, which is useful for introspection and for looking up signal numbers dynamically at runtime without hard-coding numeric constants. You can pass symbols, strings, or integers to trap interchangeably.

Thread safety

# Signals are delivered to the main thread
# Ensure your handler is thread-safe

Signals are always delivered to the main thread, even in multi-threaded programs. This means your handler runs on the main thread and should avoid blocking operations or complex synchronization that could deadlock the process.

The trap method is essential for building dependable servers and daemons that respond gracefully to operating system signals.

Signal handler best practices

A signal handler should stay small and delegate real work to the main program flow. The most reliable pattern is a flag-based approach: the handler sets a boolean and returns immediately, while the main loop checks that flag on each iteration and performs any cleanup, reload, or shutdown steps in a predictable order.

# Good: the handler flips a flag; the main loop owns the real work
@shutdown = false
trap(:TERM) { @shutdown = true }

loop do
  break if @shutdown
  process_request
end
puts "Shutting down cleanly..."

This separation matters for several reasons. Signals are delivered to the main thread in Ruby, so a handler that does heavy work can block the entire process at an unpredictable moment. A handler that mutates shared state or holds locks can deadlock if the signal arrives during a critical section. By keeping the handler to a flag flip, you avoid these hazards and keep the control flow in one place where it is easier to reason about, test, and debug.

The same flag-based approach applies to reload signals like HUP. The handler sets a flag, and the main loop reads new configuration at a safe point between operations — never while a request is mid-flight or a database transaction is open:

@reload_config = false
trap(:HUP) { @reload_config = true }

loop do
  if @reload_config
    load_configuration
    @reload_config = false
  end
  serve_request
end

Delegating shutdown and reload work to the main loop also makes the ordering explicit. You know exactly when resources are released, sockets are closed, and workers are stopped because those steps live alongside the rest of the application logic — not scattered across signal-triggered callbacks that fire at unpredictable times. A small bridge in the handler and a clear path in the main loop is easier to maintain than four separate near-identical sections that each restate the same advice.

See Also