Project

General

Profile

Actions

Feature #21359

open

Introduce `Exception#cause=` for Post-Initialization Assignment

Added by ioquatix (Samuel Williams) 2 days ago. Updated about 14 hours ago.

Status:
Open
Target version:
-
[ruby-core:122228]

Description

Ruby currently allows an exception’s cause to be explicitly set only at the time of raising using raise ..., cause: .... However, there are valid use cases where it would be convenient to set the cause when creating an exception.

The Problem

Ruby supports exception chaining via the cause: keyword during a raise, like so:

raise StandardError.new("failure"), cause: original_exception

Proposed Solution

Introduce Exception#cause= as a public setter method on Exception, allowing post-initialization assignment of the cause:

cause = RuntimeError.new("low-level error")
error = StandardError.new("higher-level error")
error.cause = cause

It would be semantically equivalent to the following implementation:

error = StandardError.new("error")
cause = RuntimeError.new("cause")

class Exception
  def cause= value
    backtrace = self.backtrace_locations

    raise self, cause: value
  rescue Exception
    self.set_backtrace(backtrace)
  end
end

p error.cause # nil
error.cause = cause
p error.cause # => #<RuntimeError: cause>

Benefits

  • Simplifies creation of rich exception objects in frameworks, tools, and tests.
  • Enables structured error chaining in asynchronous and deferred execution environments.
  • Avoids misuse of raise/rescue for control flow and metadata setting.

Related issues 1 (1 open0 closed)

Related to Ruby - Bug #21360: Inconsistent Support for `Exception#cause` in `Fiber#raise` and `Thread#raise`OpenActions

Updated by Eregon (Benoit Daloze) 1 day ago

Do you have real-world examples where you would use this, e.g. in gems?

Updated by ioquatix (Samuel Williams) 1 day ago

Yes, in Async, I want to set the cause of an exception before raising it later on a fiber.

Additionally, serialisation and deserialisation of exceptions is almost impossible without being able to set the cause, e.g. any kind of Ruby RPC.

Actions #3

Updated by Eregon (Benoit Daloze) 1 day ago

  • Related to Bug #21360: Inconsistent Support for `Exception#cause` in `Fiber#raise` and `Thread#raise` added

Updated by Eregon (Benoit Daloze) 1 day ago

ioquatix (Samuel Williams) wrote in #note-2:

Yes, in Async, I want to set the cause of an exception before raising it later on a fiber.

#21360 would be enough for that, although you'd need to wrap the exception + cause-to-be in some extra object.

Additionally, serialisation and deserialisation of exceptions is almost impossible without being able to set the cause, e.g. any kind of Ruby RPC.

WDYM by almost impossible?
Causes can't be cyclic so that's not an issue (which BTW means we would need to check that in this assignment method).
Marshal can do it I guess.
And if not raise self, cause: value + rescue but I agree that's hacky and inefficient.

Serializing exceptions properly without Marshal is probably quite hard yes, not only about the cause but also the internal backtrace representation, the backtrace_locations objects, other internal state for core exceptions that can't always be set in constructor, etc.
But is there a need for that?

Updated by ioquatix (Samuel Williams) 1 day ago

Serializing exceptions properly without Marshal is probably quite hard yes, not only about the cause but also the internal backtrace representation, the backtrace_locations objects, other internal state for core exceptions that can't always be set in constructor, etc. But is there a need for that?

Yes, serialisation with msgpack is painful and impossible to do correctly in some situations, if you have custom serialisation rules for certain object types (using a msgpack factory), you can't use Marshal.dump as part of msgpack's serialisation because it won't correctly serialise the internal objects of the exception.

Updated by Eregon (Benoit Daloze) about 14 hours ago

msgpack doesn't seem to handle recursive data structures:

irb(main):003> a=[]
=> []
irb(main):004> a<<a
=> [[...]]
irb(main):005> a
=> [[...]]
irb(main):006> a.to_msgpack
msgpack-1.8.0/lib/msgpack.rb:41:in `write': stack level too deep (SystemStackError)

So I think it's unrealistic to try to serialize/deserialize exceptions or complex objects with msgpack, it seems really meant for non-recursive simple data.

Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like0Like0