Raising a new exception in rescue also outputs the original traceback

Description

If a new exception is created and raised from within a rescue block then the traceback begins with lines from the old exception that was being rescued. What I expected and wanted was just the traceback for the new exception showing where it had been raised.

In the example below I wanted the traceback to just show where Net::HTTP.start had been called from, and not all the calls within net/http that were part of the old exception caught by the rescue. The reason for raising a new exception was to hide all the traceback from inside net/http, which obscures where it was called from.

The lines from the old traceback appear even though $@ and the the old exception’s backtrace have both been set to []. Why is this and how can it be prevented? Is the old backtrace held in another global variable that can be cleared before creating and raising the new exception?

This is using ruby 2.6.6 in Windows 10 x64

Example

Code

require 'net/http'
include Net

uri = URI('http://wrong.host/xxx')

begin
  http = Net::HTTP.start(uri.host, uri.port)
  p http
rescue StandardError => old_e
  old_e.set_backtrace([])
  $@ = []
  new_e = StandardError.new('My message')
  new_e.set_backtrace(caller)
  puts "new exception =  #{new_e.inspect}"
  puts "new backtrace = <#{new_e.backtrace.to_a.join("\n")}>"
  puts "new bt_locns  = <#{new_e.backtrace_locations.to_a.join("\n")}>"
  puts "new \$@        = <#{[email protected]_a.join("\n")}>"
  raise new_e
end

Output

D:\Docs\Dev\Ruby\Tests>test_exceptions_1.rb
new exception =  #<StandardError: My message>
new backtrace = <D:/Docs/Dev/Ruby/Tests/test_exceptions_1.rb:8:in `<main>'>
new bt_locns  = <>
new $@        = <>
Traceback (most recent call last):
        9: from D:/Docs/Dev/Ruby/Tests/test_exceptions_1.rb:9:in `<main>'
        8: from C:/Ruby26-x64/lib/ruby/2.6.0/net/http.rb:605:in `start'
        7: from C:/Ruby26-x64/lib/ruby/2.6.0/net/http.rb:925:in `start'
        6: from C:/Ruby26-x64/lib/ruby/2.6.0/net/http.rb:930:in `do_start'
        5: from C:/Ruby26-x64/lib/ruby/2.6.0/net/http.rb:945:in `connect'
        4: from C:/Ruby26-x64/lib/ruby/2.6.0/timeout.rb:103:in `timeout'
        3: from C:/Ruby26-x64/lib/ruby/2.6.0/timeout.rb:93:in `block in timeout'
        2: from C:/Ruby26-x64/lib/ruby/2.6.0/net/http.rb:947:in `block in connect'
        1: from C:/Ruby26-x64/lib/ruby/2.6.0/net/http.rb:947:in `open'
C:/Ruby26-x64/lib/ruby/2.6.0/net/http.rb:947:in `initialize': getaddrinfo: No such host is known.  (SocketError)
D:/Docs/Dev/Ruby/Tests/test_exceptions_1.rb: Failed to open TCP connection to wrong.host:80 (getaddrinfo: No such host is known. ) (SocketError)
D:/Docs/Dev/Ruby/Tests/test_exceptions_1.rb:8:in `<main>': My message (StandardError)

There’s also a ‘cause’ field on exceptions - class Exception - RDoc Documentation
This is set to $! (the current exception) when a new exception is raised.
In your case, $! is old_e, and is getting added to new_e when raised.

You can use raise new_e, cause: nil to avoid this.

Here’s a minimal example:

begin

begin
  raise StandardError.new 'begin-exception'
rescue StandardError => old_e
  new_e = Exception.new('My message')
  puts "Cause #{new_e.cause}"  # there is nothing in 'cause'
  raise new_e                  # this adds $!, i.e. old_e, as 'cause'
end

rescue Exception => new_e
  puts "Cause #{new_e.cause}"  # which we can see printed here
end
$ ruby test.rb 
Cause 
Cause begin-exception

If you replace the raise with raise new_e, cause: nil you get:

$ ruby test.rb
Cause
Cause

Thanks @pcl, I can confirm it works in my example.

I had noticed Exception’s #cause, but hadn’t realised that it gets set by raise.