Exit, Exit! Abort, Raise…Get Me Outta Here!

Tweet

Exit, Stage Left.

Every time you enter an irb session, boot a ruby script, or run a test runner you’re starting up a process. This goes for anything on your system, not just Ruby code. For instance, the same is true for shell commands like grep, cat, or tail.

We spend lots of time and effort talking about the proper way to write code and ensure that it executes efficiently, but what about how it leaves its mark on the world? There’s a precious opportunity to communicate information effectively when a process exits.

This article will shed light on why exiting properly is important and how you can do so from your Ruby programs.

Code, the Exit Kind

When a process exits, it always does so with an exit code. An exit code is a numeric value between 0 and 255. There’s a convention in the Unix world that an exit code equal to 0 means that the program was successful. Any other exit code denotes some kind of failure.

Why Do Exit Codes Matter?

Exit codes are communication.

The base case for exit codes is success, you hope that your process exits successfully every time. In the case that something went wrong, you can specify up to 255 different reasons for failure. Exit codes are crucial for providing a generic way to understand and report failure cases back to users, so that they can correct the action.

As an aside, if you’re not convinced that exit codes are something worth caring about, ponder HTTP status codes for a moment; in case of a failure response, there are a multitude of status codes denoting why a request failed that tell the user what they should do next. What do you think inspired HTTP status codes?

A Non-Ruby Example

We begin by looking at the grep command for an example and then look at doing the same thing from Ruby. Note that I’m using zsh with setopt printexitvalue to see non-zero exit codes.

$ echo foo | grep bar
zsh: done echo foo |
zsh: exit 1 grep bar

In this example we’ve printed the string ‘foo’ and grepped for ‘bar’. Obviously, it won’t match. zsh tells us that the echo command is ‘done’, aka it exited successfully. The grep command exited with a code of 1. This means it was unsuccessful. For the grep command an exit code of 1 means that it wasn’t able to match any of the inputs.

Compare that with:

$ echo foo | grep foo

foo

In this example, it was able to match on the ‘foo’ string and so it exited successfully. Also, zsh doesn’t bother telling us that everything was successful.

Now look what happens when we send an invalid option to grep:

$ echo foo | grep --whodunnit foo
grep: unrecognized option `--whodunnit'
Usage: grep [OPTION]... PATTERN [FILE]...
Try `grep --help' for more information.
zsh: done       echo foo |
zsh: exit 2     grep --whodunnit foo

This time we passed an unknown option to grep and its exit code was 2.

So we know that, for grep, if the exit code is 1 then there were no matches found; if the exit code was 2 then the command is not being used properly.

Exit-code Based Deployments

One more example showing the importance of exit codes before we dive into how to specify them from Ruby.

Have you ever deployed your code like this?

rake test && cap deploy

You may be familiar with the && in your shell. It chains two commands together specifying that the second command should only be executed if the first command exits successfully. Exiting successfully means exiting with a code of 0.

Any Ruby test runner is smart enough to exit with a non-zero exit code if there’s a test failure. This is an example of the fact that printing errors to the console is not enough. Using exit codes properly allows commands to work together in scripts and allows tasks to be automated.

The Base Case

If you’ve never written a test runner or command line program, then there’s a good chance that you’ve never needed to use one of the explicit methods to exit your Ruby program. So, what happens if you don’t use one?

The base case, when Ruby exits after executing all of your code, is to exit successfully! No surprise there. If you don’t specify otherwise your process will exit with a code of 0.

Bailing Early

The first situation where you’d want to explicit use one of the exit statements is probably when you want to exit a process before executing all of the code.

Let’s say you had a Ruby script called hasit. The script takes a pattern as the first argument and some data from STDIN. If any line in the input matches the pattern then the script exits successfully. If it gets to the end and finds no matches then it exits with a code of 1.

Here’s the script in it’s entirety.

#!/usr/bin/env ruby

pattern = ARGV.shift
data = ARGF.read

data.each_line do |line|
  exit if line.match(pattern)
end

exit 1

It exits early using Kernel.exit if a match is found. Calling that method without an argument will exit with the 0 exit code.

Fail

You can see on the last line of the script that you can pass an integer argument to Kernel.exit to specify a non-success exit code.

What if we valued an error message in this case rather than just an exit code? Kernel.abort was made for that. Calling abort and passing a String as the argument will cause your process to exit with a status code of 1 and print the string to STDERR. Let’s update the script to exit with a message:

...

data.each_line do |line|
  exit if line.match(pattern)
end

abort "No matches found"

With either of these methods our hasit script can be used properly in shell constructs like so:

$ hasit debugger lib/* && echo "Don't commit debuggers..."
$ hasit debugger lib/* || cap deploy

Cleaning Up After Yourself

Going hand-in-hand with proper exit codes is a little-known but useful feature of Ruby: at_exit handlers.

By passing a block to Kernel.at_exit you can specify code that should be executed before the process exits, a good place to clean up anything that won’t be cleaned up as part of normal execution.

...
at_exit {
  # cleanup temp files
  # close open connections
}

data.each_line do |line|
  exit if line.match(pattern)
end

abort "No matches found"

Now, no matter what method your process uses to exit, you can count on that block of code to be called before exiting. Except in one case! There is a method Kernel.exit! (notice the bang) that behaves the exact same as Kernel.exit, except it skips any at_exit handlers before exiting. This method can be used to exit a process immediately, skipping any exit handlers on the way.

Falling Through the Cracks

The last way to exit a process is an unhandled exception. This is something that you never want to happen in a production application, but it happens constantly when running tests, for instance.

An unhandled exception will set your exit code to 1 and print the exception details to STDERR.

My Classy Exit

Generally if you’re writing a command line application you’ll want to use Kernel.exit or Kernel.abort to handle your needs for exiting processes properly. In extreme cases, you’ll make use of Kernel.exit!. Full source of the hasit script is available here.

It’s good to keep in mind that any human being who will be looking at your exit codes will surely appreciate a little distinction between different errors so they can figure out where they went wrong. Giving your command the same conventions as other common commands will make your command that much easier to use.

exit 0

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.webbynode.com Felipe Coury

    Hey, did you really meant to link http://vangeijt.home.xs4all.nl/opera/css/swiss.css from the hasit script link?

  • Andrea

    Thank you for this post, very useful.

  • http://lexsheehan.blogspot.com/ Lex

    Nice post. Great ruby scripting fu.

    Note: You can also see the exit status of any bash shell command by typing following command:

    echo $?

    Thanks, again, for sharing.

  • http://github.com/sshaw sshaw

    Might be good to mention that exit just raises a SystemExit exception, which one could do directly:

    raise SystemExit, n

    For success/failure you could also say:

    exit true
    # or
    exit false