Eric / Brooklyn

Random 5

Ruby Exits

Stripe and Tap

Multi-tab Websockets

Ruby vs. Ruby

Cookies

Web Accounts

Emoji Bits

Honeypot Bots

Bad Content

JS Data Chutzpah

Sendgrid Ban

Clean URLs

Git

Concerns

Rails UJS

Extending Devise

ENV Variables

See All

Graceful Ruby Exits

Feb 2021

Exceptions, rescues, and error handling

Let’s say we’re running this simple little Ruby program:

i = 0
loop do
	exit if i == 5
	puts 'Doing something'
	i += 1
	sleep 10
end

This will print Doing something every 10 seconds, five times, and then exit.

A different, more relatable example of this might be if we we want to run something continuously until a given time.

In that case, it might look like:

y = 2030; mo = 2; d = 28; h = 17; mi = 0;
cutoff = Time.new(y, mo, d, h, mi)
loop do
	exit if Time.now > cutoff
	puts 'Doing something'
	sleep 10
end

In this case, we’ll print Doing something every 10 seconds until February 28, 2030 at 5PM. Here’s a link to a working program if you’d like to run this until 2030.

Manual Exit

In these examples, the programs would automatically exit at some point via an exit statement.

Let’s say we want to exit earlier; we can manually force an exit by running CTRL C in our Terminal.

Exit Cleanup

When our program exits, either automatically or manually, we might want to do some clean up.

Let’s say our program is a stock trading bot. When we exit, we want to make sure all the positions are closed.

Luckily, we can use Ruby’s at_exit block to handle this.

at_exit do
	# close all positions
end

This will run, as we’d expect, when the program ends. We just need to make sure this is defined before exit is called, or it won’t be found.

Exceptions

When we exit the program early via the CTRL C method, everything seems to operate normally, but we also notice that we receive a messy error message in our Terminal.

This is because CTRL C triggers a SignalException and the program terminates. When exceptions are unhandled, our Terminal spews out some stuff that’s supposed to help us out.

If we want to exit the program more gracefully during a CTRL C, we can rescue ourselves from this SignalException using a begin - rescue block.

Begin / Rescue Detour

The boilerplate begin - rescue block looks something like this:

begin
	# do something bad
	1/0
rescue
	'something bad happened'
end

If we want to know what the bad thing was, we can pass the exception object into the rescue.

begin
	# do something naughty
	1/0
rescue => e
	puts e.message
end

This will print:

divided by 0

In addition to message, there are a variety of instance methods available to call on our exception object (full_message, backtrace, etc) to give us whatever info we might want.

One other thing worth mentioning — method declarations are implicit begin blocks, so we can do something like this:

def my_method
	1 / 0
rescue
	puts 'Something bad'
end

(And so are Class and Module declarations.)

Why do we keep using 1/0 as an example? It’s a quick way to trigger an Exception, as programs (just like calculators) cannot divide numbers by zero.

One Large Caveat

If we don’t specify the Exception type on our rescue it will default to a StandardError.

So

begin
	# do something bad
	1/0
rescue
	'something bad happened'
end

is identical to

begin
	# do something bad
	1/0
rescue StandardError
	'something bad happened'
end

StandardError is an umbrella Error class, that contains many of the standard exceptions, and as we’ve seen above, ZeroDivisionError is one of these. There are a dozen or so of these children - link here.

However, the exception created by a CTRL C, SignalException, is NOT a child of StandardError, so it will not be handled by the default rescue. We have to specifically handle this scenario.

begin
	# Code here
rescue SignalException
	puts "CTRL C"
rescue
	puts "StandardError"
end

Rescuing SignalException

Now that we’re able to rescue SignalException, our terminal won’t freak out when we CTRL C.

However, we need to be careful about how we exit at this point.

Since we’re rescuing from this exception, the program will not automatically exit anymore. We need to explicitly tell it to exit.

at_exit do
	# clean up program
end

begin
	# Code here
rescue SignalException
	puts "Program terminated via CTRL C"
	exit
rescue
	puts "StandardError"
end

You might be wondering if adding that explicit exit statement was necessary. The answer is — sort of.

The Third Exit

So far, we’ve seen exits from CTRL C and exit statements in our code. Another way a program will exit is when it simply reaches the end of the program.

Back to the question above - we might get away with not calling exit in our SignalException if the program will exit on its own immediately after.

When the program exits on its own, at_exit is still called, and everything goes as planned.

But it pays to be explicit in this case, because there’s a chance there could be something that happens after the begin - rescue block, but before the program naturally concludes and exits.

For example:

at_exit do
	puts "in at_exit"
end

begin
	sleep 10
rescue SignalException
	puts "Program terminated via CTRL C"
	# no exit call here
end

sleep 100

This program will initially sleep for 10 seconds. If we call CTRL C during this 10 seconds, we will be rescued, but we have removed the exit call.

Now the program won’t exit right away. at_exit won’t run for 100 seconds.

If we add back the exit call in our SignalException, we can guarantee that we immediately exit when CTRL C is called.

at_exit do
	puts "in at_exit"
end

begin
	sleep 10
rescue SignalException
	puts "Program terminated via CTRL C"
	exit
end

One Last Rescue

Everything should be working nicely now. For clarity, we’ll add one last rescue statement.

at_exit do
	puts "in at_exit"
end

begin
	sleep 10
	exit
rescue SignalException
	puts "Program terminated via CTRL C"
	exit
rescue SystemExit
	puts "Program terminated via exit command"
	exit
rescue => e
	puts "StandardError"
	puts e.message
end

This step isn’t completely necessary, but I think it helps understand what’s happening. We’ve added a SystemExit rescue, which captures any exit statement in our begin block. As you may have expected, SystemExit is not a child of StandardError, so we have to explicitly declare it.

This is helpful because we receive information about how the program exited. Every time the program exits, we’ll know whether it came from an exit call or from a CTRL C.

And again, since we’re capturing this in a rescue block, we have to explicitly exit for the same reasons as mentioned above.

The end

Our program can exit via (1) an exit statement in the begin block, (2) a CTRL C user input, or (3) by reaching the end of the program.

(1) is captured by a SystemExit and explicitly exited. (2) is captured by a SignalException and explicitly exited.

(1),(2), and (3) find their way to the at_exit block, allowing us to clean things up.