As developers, we spend so much time on making our programs run that it's easy to overlook how they exit. And it's important! When your programs behave correctly on exit, it makes them a lot easier to manage and makes them more likely to work with standard devops tools.

There are a lot of ways that you can exit a Ruby program. I've shown some of them below. In this post we'll discuss the details of each of these, and how they can be used to make your app a well-behaved unix citizen.

# You can exit by calling a method
exit
exit!
abort("She cannot take any more of this, Captain!")

# ...or by failing to catch an exception
raise("Destroyed...")
fail

# ...or by letting the program end naturally. :)

Exit codes, and YOU

Every program you run on Linux and OSX returns an exit status code when it's finished running. You don't normally see the exit code. It's used by the OS behind-the-scenes to determine if the program exited normally or if there was an error.

If you're using bash, you can see the exit code of the program that you just ran by examining the $? environment variable. The example below shows the exit code for a failure, then for a success.

% ls this-file-doesnt-exist
ls: this-file-doesnt-exist: No such file or directory
blog% echo $?
1
% ls .
bm.rb 
% echo $?
0

Generally, the rule for unix-based systems is that zero means success. Nonzero means failure.

If you've ever used the &&   "operator" in bash, then  you've used status codes. In the example below, we tell bash to run program B only if program A was successful.

% a && b

You can use this trick in a lot of ways. For example, you might want to precompile assets, and upload them to a CDN.

blog% rake assets:precompile && rake cdn:upload_assets

Specifying an exit status with Ruby

Ruby takes care of a lot of the exit status stuff for us. If your program exits normally, it returns a "success" status. If it fails due to an uncaught exception, it gives a "failure" status. Below I'm showing the exit code for a script that raised an exception.

% ruby err.rb
err.rb:1:in `<main>': goodbye (RuntimeError)
tmp% echo $?
1

But what if you didn't want any uncaught exceptions? What if you wanted to exit cleanly, but still return a "failed" error code? Moreover, what if you wanted to return a custom code with an error message?

Fortunately, it's quite easy. Just pass an argument into the exit function. The argument can be a boolean or an integer. If it's boolean, then true means success. If it's an integer than 0 means success.

exit(true) # Exits with "success" code
exit(0) # Exits with "success" code

exit(false) # Exits with "failure" code
exit(1) # Exits with "failure" code

exit(436) # Custom failure error code

How exit works under the hood

The exit method simply raises a SystemExit exception. If it's uncaught, the program aborts just like it would with any other uncaught exception. That's pretty cool,  but there are some interesting consequences.

If you were to swallow all SystemExit exceptions, you'd break the exit method. I've shown this in the example below.

begin
  exit 100
rescue SystemExit => e
  puts "Tried to exit with status #{ e.status }"
end

puts "...but it never exited, because we swallowed the exception"

# Outputs:
# Tried to exit with status 100
# ...but it never exited, because we swallowed the exception

This is yet another reason never to rescue Exception. Because SystemExit inherits from Exception, swallowing Exception will cause exit to break.

begin
  exit
rescue Exception # never do this
  puts "I just broke the exit function!"
end

Human-readable error messages

While exit codes are great for machines, we humans often prefer a little bit of explanatory text. Fortunately, the OS provides us with an output stream specifically for things like error messages. Yep, I'm talking about STDERR.

You can write to STDERR just like you write to any IO object. In the example below I'm writing an error message and exiting with an "error" status.

STDERR.puts("ABORTED! You forgot to BAR the BAZ")
exit(false)

This being Ruby, there is of course a more concise way to write to stdout and exit with an error code. Just use the abort   method.

# Write the message to STDERR and exit with an error status code.
abort("ABORTED! You forgot to BAR the BAZ")

Callbacks via at_exit

Ruby lets you register handlers which can be called whenever the program exits. There can be more than one of them - they're called in the opposite order that they were registered in. Here's what it looks like:

at_exit do 
  puts "handler 1"
end

at_exit do 
  puts "handler 2"
end

# Outputs "handler2\nhandler1" on exit

The exit handler can override the exit code. You do this by calling exit from within the handler. Even though it seems like this should cause an infinite loop, it doesn't. :)

at_exit do 
  exit 100
end

exit 0

# This program exits with a code of 100

If you want to exit without calling any callbacks, you can do so via the exit! method. But keep in mind that this may cause problems if you have gems or other third-party code that depends on those callbacks.

An interesting aside

It turns out that lots of libraries use at_exit in inventive - and some might say hacky - ways. For example, Sinatra uses an at_exit hook to as a way to boot up the web app. This allows it to be sure that all your code has been loaded before it starts the server. Here's what it looks like:

module Sinatra
  class Application < Base

   ...

  at_exit { Application.run! if $!.nil? && Application.run? }
end

There's a great post about this on the Arkency blog: Are we abusing at_exit?