April 18, 2018
This blog is part of our Ruby 2.5 series.
Let's see what happens when an exception is raised inside a thread.
division_thread = Thread.new do
puts "Calculating 4/0 in division_thread"
puts "Result is: #{4/0}"
puts "Exiting from division_thread"
end
sleep 1
puts "In the main thread"
Execution of it looks like this.
$ RBENV_VERSION=2.4.0 ruby thread_example_1.rb
Calculating 4/0 in division_thread
In the main thread
Note that the last two lines from the block were not printed. Also notice that after failing in the thread the program continued to run in main thread. That's why we got the message "In the main thread".
This is because the default behavior of Ruby is to silently ignore exceptions in threads and then to continue to execute in the main thread.
If we want an exception in a thread to stop further processing both in the
thread and in the main thread then we can enable Thread[.#]abort_on_exception
on that thread to achieve that.
Notice that in the below code we are using Thread.current
.
division_thread = Thread.new do
Thread.current.abort_on_exception = true
puts "Calculating 4/0 in division_thread"
puts "Result is: #{4/0}"
puts "Exiting from division_thread"
end
sleep 1
puts "In the main thread"
$ RBENV_VERSION=2.4.0 ruby thread_example_2.rb
Calculating 4/0 in division_thread
thread_example_2.rb:5:in `/': divided by 0 (ZeroDivisionError)
from thread_example_2.rb:5:in `block in <main>'
As we can see once an exception was encountered in the thread then processing stopped on both in the thread and in the main thread.
Note that Thread.current.abort_on_exception = true
activates this behavior
only for the current thread.
If we want this behavior globally for all the threads then we need to use
Thread.abort_on_exception = true
.
Let's run the original code with --debug
option.
$ RBENV_VERSION=2.4.0 ruby --debug thread_example_1.rb
thread_example_1.rb:1: warning: assigned but unused variable - division_thread
Calculating 4/0 in division_thread
Exception `ZeroDivisionError' at thread_example_1.rb:3 - divided by 0
Exception `ZeroDivisionError' at thread_example_1.rb:7 - divided by 0
thread_example_1.rb:3:in `/': divided by 0 (ZeroDivisionError)
from thread_example_1.rb:3:in `block in <main>'
In this case the exception is printed in detail and the code in main thread was not executed.
Usually when we execute a program with --debug
option then the behavior of the
program does not change. We expect the program to print more stuff but we do not
expect behavior to change. However in this case the --debug
option changes the
behavior of the program.
If a thread raises an exception and abort_on_exception
and $DEBUG
flags are
not set then that exception will be processed at the time of joining of the
thread.
division_thread = Thread.new do
puts "Calculating 4/0 in division_thread"
puts "Result is: #{4/0}"
puts "Exiting from division_thread"
end
division_thread.join
puts "In the main thread"
$ RBENV_VERSION=2.4.0 ruby thread_example_3.rb
Calculating 4/0 in division_thread
thread_example_3.rb:3:in `/': divided by 0 (ZeroDivisionError)
from thread_example_3.rb:3:in `block in <main>'
Both Thread#join
and Thread#value
will stop processing in the thread and in
the main thread once an exception is encountered.
Almost 6 years ago, Charles Nutter (headius) had proposed that the exceptions raised in threads should be automatically logged and reported, by default. To make his point, he explained issues similar to what we discussed above about the Ruby's behavior of silently ignoring exceptions in threads. Here is the relevant discussion on his proposal.
Following are some of the notable points discussed.
Thread[.#]abort_on_exception
, by default, is not always a good
idea.Thread#join
or Thread#value
. Such threads gets garbage collected.
Should it report the thread-killing exception at the time of garbage
collection if such a flag is enabled?Warning#warn
or redirect to STDERR device while reporting?Charles Nutter suggested that a configurable global flag
Thread.report_on_exception
and instance-level flag
Thread#report_on_exception
should be implemented having its default value as
true
. When set to true
, it should report print exception information.
Matz and other core members approved that Thread[.#]report_on_exception
can be
implemented having its default value set to false
.
Charles Nutter, Benoit Daloze and other people demanded that it should be true
by default so that programmers can be aware of the silently disappearing threads
because of exceptions.
Shyouhei Urabe advised that
due to some technical challenges, the default value should be set to false
so
as this feature could land in Ruby. Once this feature is in then the default
value can be changed in a later release.
Nobuyoshi Nakada (nobu) pushed an
implementation
for Thread[.#]report_on_exception
with a default value set to false
. It was
released in Ruby 2.4.0.
Let's try enabling report_on_exception
globally using
Thread.report_on_exception
.
Thread.report_on_exception = true
division_thread = Thread.new do
puts "Calculating 4/0 in division_thread"
puts "Result is: #{4/0}"
puts "Exiting from division_thread"
end
addition_thread = Thread.new do
puts "Calculating nil+4 in addition_thread"
puts "Result is: #{nil+4}"
puts "Exiting from addition_thread"
end
sleep 1
puts "In the main thread"
$ RBENV_VERSION=2.4.0 ruby thread_example_4.rb
Calculating 4/0 in division_thread
#<Thread:0x007fb10f018200@thread_example_4.rb:3 run> terminated with exception:
thread_example_4.rb:5:in `/': divided by 0 (ZeroDivisionError)
from thread_example_4.rb:5:in `block in <main>'
Calculating nil+4 in addition_thread
#<Thread:0x007fb10f01aca8@thread_example_4.rb:9 run> terminated with exception:
thread_example_4.rb:11:in `block in <main>': undefined method `+' for nil:NilClass (NoMethodError)
In the main thread
It now reports the exceptions in all threads. It prints that the
Thread:0x007fb10f018200
was
terminated with exception: divided by 0 (ZeroDivisionError)
. Similarly,
another thread Thread:0x007fb10f01aca8
was
terminated with exception: undefined method '+' for nil:NilClass (NoMethodError)
.
Instead of enabling it globally for all threads, we can enable it for a
particular thread using instance-level Thread#report_on_exception
.
division_thread = Thread.new do
puts "Calculating 4/0 in division_thread"
puts "Result is: #{4/0}"
puts "Exiting from division_thread"
end
addition_thread = Thread.new do
Thread.current.report_on_exception = true
puts "Calculating nil+4 in addition_thread"
puts "Result is: #{nil+4}"
puts "Exiting from addition_thread"
end
sleep 1
puts "In the main thread"
In the above case we have enabled report_on_exception
flag just for
addition_thread
.
Let's execute it.
$ RBENV_VERSION=2.4.0 ruby thread_example_5.rb
Calculating 4/0 in division_thread
Calculating nil+4 in addition_thread
#<Thread:0x007f8e6b007f70@thread_example_5.rb:7 run> terminated with exception:
thread_example_5.rb:11:in `block in <main>': undefined method `+' for nil:NilClass (NoMethodError)
In the main thread
Notice how it didn't report the exception which killed thread division_thread
.
As expected, it reported the exception that killed thread addition_thread
.
With the above changes ruby reports the exception as soon as it encounters. However if these threads are joined then they will still raise exception.
division_thread = Thread.new do
Thread.current.report_on_exception = true
puts "Calculating 4/0 in division_thread"
puts "Result is: #{4/0}"
puts "Exiting from division_thread"
end
begin
division_thread.join
rescue => exception
puts "Explicitly caught - #{exception.class}: #{exception.message}"
end
puts "In the main thread"
$ RBENV_VERSION=2.4.0 ruby thread_example_6.rb
Calculating 4/0 in division_thread
#<Thread:0x007f969d00d828@thread_example_6.rb:1 run> terminated with exception:
thread_example_6.rb:5:in `/': divided by 0 (ZeroDivisionError)
from thread_example_6.rb:5:in `block in <main>'
Explicitly caught - ZeroDivisionError: divided by 0
In the main thread
See how we were still be able to handle the exception raised in
division_thread
above after joining it despite it reported it before due to
Thread#report_on_exception
flag.
Benoit Daloze (eregon) strongly advocated that both
the Thread.report_on_exception
and Thread#report_on_exception
should have
default value as true
. Here is the
relevant feature request.
After approval from Matz, Benoit Daloze pushed the implementation by fixing the failing tests and silencing the unnecessary verbose warnings.
It was released as part of Ruby 2.5.
Now in ruby 2.5 we can simply write like this.
division_thread = Thread.new do
puts "Calculating 4/0 in division_thread"
puts "Result is: #{4/0}"
puts "Exiting from division_thread"
end
addition_thread = Thread.new do
puts "Calculating nil+4 in addition_thread"
puts "Result is: #{nil+4}"
puts "Exiting from addition_thread"
end
sleep 1
puts "In the main thread"
Let's execute it with Ruby 2.5.
$ RBENV_VERSION=2.5.0 ruby thread_example_7.rb
Calculating 4/0 in division_thread
#<Thread:0x00007f827689a238@thread_example_7.rb:1 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
1: from thread_example_7.rb:3:in `block in <main>'
thread_example_7.rb:3:in `/': divided by 0 (ZeroDivisionError)
Calculating nil+4 in addition_thread
#<Thread:0x00007f8276899b58@thread_example_7.rb:7 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
thread_example_7.rb:9:in `block in <main>': undefined method `+' for nil:NilClass (NoMethodError)
In the main thread
We can disable the thread exception reporting globally using
Thread.report_on_exception = false
or for a particular thread using
Thread.current.report_on_exception = false
.
In addition to this feature, Charles Nutter also suggested that it will be good if there exists a callback handler which can accept a block to be executed when a thread dies due to an exception. The callback handler can be at global level or it can be for a specific thread.
Thread.on_exception do
# some stuff
end
In the absence of such handler libraries need to resort to custom code to handle exceptions. Here is how Sidekiq handles exceptions raised in threads.
Important thing to note is that report_on_exception
does not change behavior
of the code. It does more reporting when a thread dies and when it comes to
thread dies more reporting is a good thing.
If this blog was helpful, check out our full blog archive.