Project

General

Profile

Actions

Bug #16829

open

Exceptions raised from within an enumerated method lose part of their stacktrace

Added by doliveirakn (Kyle d'Oliveira) over 2 years ago. Updated 3 months ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:98135]

Description

Consider the following code:

class Test
  include Enumerable

  def each(&block)
    raise "Boom"
  end
end

def execution_method_a
  Test.new.to_enum(:each).next
end

def execution_method_b
  Test.new.each do
    # Never gets run
  end
end

begin
  execution_method_a
rescue RuntimeError => e
  puts "Using to_enum and next"
  puts e.message
  puts e.backtrace
end


begin
  execution_method_b
rescue RuntimeError => e
  puts "Calling a block directly"
  puts e.message
  puts e.backtrace
end

When this file (located at lib/script.rb) is run the result is:

Using to_enum and next
Boom
lib/script.rb:5:in `each'
lib/script.rb:1:in `each'
Calling a block directly
Boom
lib/script.rb:5:in `each'
lib/script.rb:14:in `execution_method_b'
lib/script.rb:29:in `<main>'

This is a little unusual. Effectively, if we create an enumerator and use next to iterate through the results, the backtrace is modified to the point where the calling method(s) are entirely lose. Notice when the each method is used directly and an exception is thrown, we see execution_method_b present in the stacktrace, but if we use next we do not see execution_method_a present at all.

This means that if there is some code that uses the enumerator/next approach deep within a callstack, the exception that comes out does not have any crucial information of where the call originated from.

Updated by marcandre (Marc-Andre Lafortune) over 2 years ago

I believe this is due to the fact that next's implementation uses a Fiber.

If you use to_a instead of next, you will get the stacktrace you were hoping for.

If you replace your definition of execution_method_a with:

$fiber = Fiber.new do
  raise 'Boom'
end

def execution_method_a
  $fiber.resume
end

You'll also get a shorter stack trace then you'd like; the exception is raised within a fiber and doesn't know where it was resumed from.

It would be a good idea to specify this in the doc of next, peek, etc.

Updated by marcandre (Marc-Andre Lafortune) over 2 years ago

I've made by best to improve the documentation in 7bde981.

@nobu (Nobuyoshi Nakada): is it feasible to improve stacktraces raised within fibers?

Updated by KyleFromKitware (Kyle Edwards) 3 months ago

Came here to say that I ran into this same issue and found it very frustrating. I had to go over a lot of code to find the function that called Enumerator#next. Would love to see better stack traces in the future.

Updated by KyleFromKitware (Kyle Edwards) 3 months ago

For anyone looking for a workaround, this seems to work pretty well:

# assume `enum` is an `Enumerator`
begin
  enum.next
rescue StopIteration
  raise
rescue
  $!.set_backtrace($!.backtrace + caller)
  raise
end

This will give the exception a much more complete (albeit slightly fabricated) stack trace.

Updated by KyleFromKitware (Kyle Edwards) 3 months ago

is it feasible to improve stacktraces raised within fibers?

As a more general solution, I'm wondering if exceptions could have not just a single stack trace, but an append-only list of stack traces that gets appended every time the exception is thrown or re-thrown. (It could retain backtrace and set_backtrace for compatibility.)

Actions

Also available in: Atom PDF