diff --git a/lib/concurrent-ruby/concurrent/promises.rb b/lib/concurrent-ruby/concurrent/promises.rb index c5df8fe9c..7010c1951 100644 --- a/lib/concurrent-ruby/concurrent/promises.rb +++ b/lib/concurrent-ruby/concurrent/promises.rb @@ -1014,9 +1014,11 @@ def exception(*args) raise Concurrent::Error, 'it is not rejected' unless rejected? raise ArgumentError unless args.size <= 1 reason = Array(internal_state.reason).flatten.compact + locations_supported = RUBY_VERSION >= '3.4' + callsites = locations_supported ? caller_locations : caller if reason.size > 1 ex = Concurrent::MultipleErrors.new reason - ex.set_backtrace(caller) + ex.set_backtrace(callsites) ex else ex = if reason[0].respond_to? :exception @@ -1024,7 +1026,11 @@ def exception(*args) else RuntimeError.new(reason[0]).exception(*args) end - ex.set_backtrace Array(ex.backtrace) + caller + if locations_supported && (locations = ex.backtrace_locations) + ex.set_backtrace locations + callsites + else + ex.set_backtrace Array(ex.backtrace) + callsites.map(&:to_s) + end ex end end diff --git a/spec/concurrent/promises_spec.rb b/spec/concurrent/promises_spec.rb index 2aa88fdfa..4fd5df976 100644 --- a/spec/concurrent/promises_spec.rb +++ b/spec/concurrent/promises_spec.rb @@ -566,6 +566,44 @@ def behaves_as_delay(delay, value) expect(exception).to be_a Concurrent::MultipleErrors expect(strip_methods[backtrace] - strip_methods[exception.backtrace]).to be_empty end + + it 'sets a consistent backtrace and backtrace_locations for an exception with captured locations' do + skip 'backtrace_locations is only populated on Ruby >= 3.4' if RUBY_VERSION < '3.4' + + raised = (raise TypeError, 'boom' rescue $!) + exception = rejected_future(raised).exception + + expect(exception).to be_a TypeError + expect(exception.backtrace).not_to be_nil + expect(exception.backtrace_locations).not_to be_nil + expect(exception.backtrace_locations.map(&:to_s)).to eq exception.backtrace + end + + it 'preserves a String-only backtrace when no locations are available' do + skip 'backtrace_locations is only populated on Ruby >= 3.4' if RUBY_VERSION < '3.4' + + string_only = TypeError.new + string_only.set_backtrace %W[/a /b /c] + expect(string_only.backtrace_locations).to be_nil + + exception = rejected_future(string_only).exception + + expect(exception).to be_a TypeError + expect(exception.backtrace).not_to be_nil + expect(exception.backtrace_locations).to be_nil + expect(exception.backtrace).to start_with %W[/a /b /c] + end + + it 'sets a consistent backtrace and backtrace_locations for MultipleErrors' do + skip 'backtrace_locations is only populated on Ruby >= 3.4' if RUBY_VERSION < '3.4' + + exception = (rejected_future(TypeError.new) & rejected_future(TypeError.new)).exception + + expect(exception).to be_a Concurrent::MultipleErrors + expect(exception.backtrace).not_to be_nil + expect(exception.backtrace_locations).not_to be_nil + expect(exception.backtrace_locations.map(&:to_s)).to eq exception.backtrace + end end describe 'ResolvableEvent' do