Skip to content

Store failure exception with cause on result#220

Open
branson-simplethread wants to merge 1 commit into
sunny:mainfrom
branson-simplethread:capture-exception-on-fail
Open

Store failure exception with cause on result#220
branson-simplethread wants to merge 1 commit into
sunny:mainfrom
branson-simplethread:capture-exception-on-fail

Conversation

@branson-simplethread

Copy link
Copy Markdown

Hello! 👋

The goal of this change is to make raised exceptions available to the caller of Actor#result. Additionally, exceptions rescued with fail_on are captured as the cause of the configured failure class.

I'm not too attached to the implementation here, so I'd appreciate your thoughts! But I found this necessary for some work I'm doing and had to patch ServiceActor::Failable.

@branson-simplethread
branson-simplethread force-pushed the capture-exception-on-fail branch from 92040c3 to 582fd62 Compare June 14, 2026 19:28
@branson-simplethread
branson-simplethread force-pushed the capture-exception-on-fail branch from 582fd62 to 0a437cf Compare June 14, 2026 19:34

@sunny sunny left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Before we merge this, can you explain a bit how you are patching ServiceActor::Failable?

@viralpraxis

Copy link
Copy Markdown
Contributor

This seems to be an incompatible change. For instance, it breaks actors with something like

begin
rescue ExceptionClass => e
  fail! exception: e
end

without the patch, the result would have {exception: e}; with the patch e would be ServiceActor::Failure

@branson-simplethread

branson-simplethread commented Jun 15, 2026

Copy link
Copy Markdown
Author

can you explain a bit how you are patching ServiceActor::Failable?

Sure! It's basically what is in this PR. Without this change the exception is lost.

module ServiceActor::Failable::PrependedMethods
  def _call
    super
  rescue *self.class.fail_ons => e
    fail!(error: e.message, exception: e)
  end
end

The exception is then available at result[:exception].


For context, here's a simplified Rails example demonstrating how this is useful.

class UpdatePost < ApplicationActor
  input :id
  input :attributes

  fail_on ActiveRecord::RecordInvalid

  def call
    Post.find(id).update!(attributes)
  end
end

class PostsController < ApplicationController
  def update
    result = UpdatePost.result(id: params[:id], attributes: post_params)
    if result.success?
      head :ok
    elsif result[:exception].is_a?(ActiveRecord::RecordInvalid)
      render json: { errors: result[:exception].record.errors.full_messages }, status: :unprocessable_content
    else
      render json: { error: 'Could not update post' }, status: :bad_request
    end
  end
end

@branson-simplethread

branson-simplethread commented Jun 15, 2026

Copy link
Copy Markdown
Author

This seems to be an incompatible change. For instance, it breaks actors with something like

begin
rescue ExceptionClass => e
  fail! exception: e
end

without the patch, the result would have {exception: e}; with the patch e would be ServiceActor::Failure

True! I guess the thing to decide is whether to:

  • make this a breaking change, with an exception: passed manually to fail! now accessible at result.exception.cause rather than result[:exception]. That would make this a public feature that could be used in a more local manner than actor-wide fail_on.
  • pass rescued fail_on exception from Failable to Result in a more private way

@viralpraxis

Copy link
Copy Markdown
Contributor

This seems to be an incompatible change. For instance, it breaks actors with something like

begin
rescue ExceptionClass => e
  fail! exception: e
end

without the patch, the result would have {exception: e}; with the patch e would be ServiceActor::Failure

True! I guess the thing to decide is whether to:

* make this a breaking change, with an `exception:` passed manually to `fail!` now accessible at `result.exception.cause` rather than `result[:exception]`. That would make this a public feature that could be used in a more local manner than actor-wide `fail_on`.

* pass rescued `fail_on` exception from `Failable` to `Result` in a more private way

Perhaps we could just emit a warning first? we used to follow this way e.g. here: #138

@sunny

sunny commented Jul 10, 2026

Copy link
Copy Markdown
Owner

This seems to be an incompatible change. For instance, it breaks actors with something like

begin
rescue ExceptionClass => e
  fail! exception: e
end

without the patch, the result would have {exception: e}; with the patch e would be ServiceActor::Failure

Another way to lower the surface of the incompatible change would be to only fill the exception when using fail_on. When using fail! it would stay nil.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants