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?