Kernel#at_exit
at_exit { block } at_exit registers a block of code that Ruby will execute when the program terminates. The registered blocks run in reverse order of registration, after all other Ruby code has finished. This is useful for cleanup tasks like closing file handles, flushing buffers, or logging shutdown events.
Syntax
at_exit { block }
The block passed to at_exit receives no arguments, and its return value is discarded by the runtime. The method itself returns a Proc object representing the registered handler, though most callers ignore that return value since the registration is the meaningful side effect. You can register as many handlers as you need, and they will all execute at shutdown in the order described below.
Parameters
at_exit takes no named parameters—it only accepts a block.
Examples
Basic usage
at_exit { puts "Program is shutting down..." }
puts "Running main program..."
# Running main program...
# Program is shutting down...
This example shows the simplest possible at_exit hook — printing a message after the main body finishes. In a real program the block typically handles something more useful, such as closing a file descriptor, flushing a buffer, or writing a final log entry. The mechanics stay the same regardless of complexity: register the block and Ruby calls it when the process ends normally.
Multiple at_exit handlers
at_exit { puts "First handler" }
at_exit { puts "Second handler" }
at_exit { puts "Third handler" }
# Output:
# Third handler
# Second handler
# First handler
Handlers execute in last-in-first-out order: the most recently registered block runs first, and blocks registered earlier run later. This ordering can be useful when cleanup steps have dependencies — register the foundational handler first so it runs last, then register dependent handlers that need the foundational resources to still be available when they execute.
Cleanup with file handles
log_file = File.open('application.log', 'a')
at_exit do
log_file.puts "[\#{Time.now}] Application terminated"
log_file.close
end
puts "Logging startup..."
# Logging startup...
Opening a log file at the start of the program and registering a close handler with at_exit is a practical cleanup pattern. The file stays open for the duration of the run, and the exit hook ensures it gets flushed and closed even if the program ends unexpectedly — though note that abort and certain signals bypass at_exit blocks entirely, so this is not a catch-all safety net.
Common Patterns
Graceful shutdown handlers: Register cleanup code that must run regardless of how the program exits.
at_exit do
puts "Final cleanup complete"
end
Order matters: Multiple at_exit blocks run in LIFO order. Register critical handlers first.
running cleanup at program exit
at_exit is a good fit when the code needs one last chance to tidy up after the main work is done. That often means closing files, flushing logs, or printing a short message that should only appear when the process is truly ending. The block runs late in the lifecycle, so it should stay small and predictable. If the cleanup gets complicated, it may be clearer to put the work in a named helper and call that from the exit hook.
The reverse registration order is also worth remembering because it lets later handlers run first. That can be helpful when one cleanup step depends on another being ready. By keeping the handlers short and the order deliberate, the program can leave a cleaner trail when it shuts down.
That pattern is especially useful in small scripts and command-line tools where the final actions are simple but still important. The block can write a last message, close a file, or flush a resource without adding much ceremony to the main code path. For example, a script that processes a file can use the exit hook to report how many lines it handled:
line_count = 0
at_exit { puts "Processed #{line_count} line(s)" }
File.foreach(ARGV[0]) do |line|
line_count += 1
# ... work with line ...
end
Because the hook runs at the end, it works best when the work is tiny and the intent is easy to see.
It is worth keeping those handlers close to the code they support so the cleanup path does not feel hidden. A reader should be able to spot the exit hook and understand why it is there without hunting through the file. That keeps shutdown behavior part of the same story as the work that created the resource in the first place.