Reason #154 • June 3rd, 2026

Running code at shutdown via at_exit

In Ruby, we can register blocks of code to be run when the program exits using the at_exit method. This is useful for performing cleanup tasks, printing a goodbye message, or even somewhat unintuitively running the actual program.

Ruby
at_exit do
  puts "Goodbye!"
end

puts "Hello, world!"

# Output:
# Hello, world!
# Goodbye!
    

You can register multiple at_exit blocks, and they will be executed in reverse order of registration:

Ruby
at_exit do
  puts "first goodbye!"

  at_exit do
    puts "nested goodbye!"
  end
end

at_exit do
  puts "last goodbye!"
end

puts "Hello, world!"

# Output:
# Hello, world!
# last goodbye!
# first goodbye!
# nested goodbye!
    

Let's look at an example of using at_exit to clean up temporary files created during a program's execution:

Ruby
require "net/http"
require "json"

NUMBER_OF_POSTS = ENV.fetch("NUMBER_OF_POSTS", 10).to_i
DOWNLOADS_DIR = "tmp-downloads"

at_exit do
  puts
  puts "Cleaning up temporary files..."
  require "fileutils"
  FileUtils.rm_rf(DOWNLOADS_DIR)
end

Dir.mkdir(DOWNLOADS_DIR) unless Dir.exist?(DOWNLOADS_DIR)

(1..NUMBER_OF_POSTS).each do |i|
  puts "Downloading post #{i}..."
  response = Net::HTTP.get("jsonplaceholder.typicode.com", "/posts/#{i + 1}")
  File.write("#{DOWNLOADS_DIR}/post_#{i + 1}.json", response)
  sleep 0.1 # Simulate a longer download time
end

posts_word_tally = Dir.glob("#{DOWNLOADS_DIR}/*.json").each_with_object({}) do |path, tally|
  post = JSON.load_file(path)
  post.fetch("body").split.tally(tally)
end

puts "Top ten words across all #{NUMBER_OF_POSTS} posts:"
posts_word_tally.sort_by { |_, count| -count }.first(10).each do |word, count|
  puts "#{word}: #{count}"
end
    

Now, if the program is interrupted (e.g. via Ctrl+C), the at_exit block will still run, ensuring that the temporary files are cleaned up, with an output like:

Downloading post 1...
Downloading post 2...
Downloading post 3...
^C
Cleaning up temporary files...

scratchpad.rb:19:in 'Kernel#sleep': Interrupt
  from scratchpad.rb:19:in 'block in <main>'
  from scratchpad.rb:16:in 'Range#each'
  from scratchpad.rb:16:in '<main>'

If the program is allowed to fully execute, it will finally print:

Top ten words across all 10 posts:
aut: 9
qui: 9
et: 8
ut: 8
quis: 7
voluptatem: 5
molestiae: 5
sed: 5
autem: 4
ea: 4

Cleaning up temporary files...

Either way, we don't have to worry about leaving temporary files around!

Another rather unintuitive use case for at_exit is to execute a program's main logic through it. For example, minitest uses it to run tests after they have been defined. The mini web framework sinatra uses it to start its web server after all routes have been initialized.

History

at_exit shipped in Ruby 1.2 in 1998.

Ruby 1.4 in 1999 made the runner handle exceptions from exit handlers, so one failing handler would not simply stop shutdown processing.

Ruby 1.8.1 in 2003 tightened the API by rejecting at_exit calls without a block.

Since then the API has stayed stable.

Reason #155 ?