Ash 3.0 - adding errors in `before_transaction` hook does not abort further hooks execution

Currently migrating from 2.x to 3.x!

My test suite detected a behavior change that I couldn’t explain by looking at the upgrade doc or the new documentation, so I’m really not sure how I should handle this.

I got an update action that starts the email change process, which is basically just my Ash version of what is found in the mix phx.gen.auth generator.

If the email is already taken, I add an error with Changeset.add_error/2 in the first before_transaction hook:

update :request_email_change do
  description """
  Create a user token for changing the credential's email and send a confirmation email.
  """

  accept [:email]

  argument :current_password, :string, allow_nil?: false, sensitive?: true

  argument :update_email_url_fun, :function,
    allow_nil?: false,
    description: """
    A function taking the confirmation token as its only argument and returning the URL to send to the user.
    """

  validate changing(:email)

  change fn changeset, %{actor: actor} ->
    changeset
    |> Changeset.before_transaction(fn changeset ->
      case changeset
           |> Changeset.get_attribute(:email)
           |> UserCredential.get_by_email(actor: actor) do
        {:ok, _} ->
          Changeset.add_error(
            changeset,
            Ash.Error.Changes.InvalidArgument.exception(
              field: :email,
              message: "has already been taken"
            )
          )

        {:error, _} ->
          changeset
      end
    end)
    |> Changeset.before_transaction(&validate_current_password/1)
    |> Changeset.before_action(fn changeset ->
      current_email = Changeset.get_data(changeset, :email)
      update_email_url_fun = Changeset.get_argument(changeset, :update_email_url_fun)

      {:ok, applied_credential} = Ash.Changeset.apply_attributes(changeset)

      Accounts.deliver_credential_update_email_instructions(
        Ash.load!(applied_credential, [:user], actor: actor),
        current_email,
        update_email_url_fun
      )

      # clear the change so no update happens yet
      Changeset.clear_change(changeset, :email)
    end)
  end
end

I’m almost 100% positive that previously this prevented the execution of the next hooks.

The following test makes sure the email validation uniqueness works, and was passing before upgrading to 3.0:

test "validates email uniqueness", %{user: user, credential: credential} do
  %{email: email} = user_registration_fixture().credential

  assert_raise Ash.Error.Invalid,
               ~r/email: has already been taken/,
               fn ->
                 UserCredential.request_email_change!(
                   credential,
                   email,
                   valid_user_credential_password(),
                   &update_email_url_fun/1,
                   actor: user
                 )
               end
end

Instead of raising the expected Ash.Error.Invalid as before, I know get a Ash.Error.Unknown because there is a match error in the before_action hook (when matching {:ok, applied_credential}), despite my InvalidArgument error being present in the changeset:

%Ash.Error.Unknown{
  changeset: "#Changeset<>",
  errors: [
    %Ash.Error.Unknown.UnknownError{
      error: %MatchError{
        term: {:error,
         #Ash.Changeset<
           domain: TalentIdeal.Accounts,
           action_type: :update,
           action: :request_email_change,
           attributes: %{
             email: #Ash.CiString<"user-576460752303423358@example.com">
           },
           relationships: %{},
           arguments: %{ ... },
           errors: [
             %Ash.Error.Changes.InvalidArgument{
               field: :email,
               message: "has already been taken",
               value: nil,
               splode: nil,
               bread_crumbs: [],
               vars: [],
               path: [],
               stacktrace: #Splode.Stacktrace<>,
               class: :invalid
             }
           ],
           data: #TalentIdeal.Accounts.UserCredential< ... >,
           valid?: false
         >}
      },
      field: nil,
      splode: Ash.Error,
      bread_crumbs: [],
      vars: [],
      path: [],
      stacktrace: #Splode.Stacktrace<>,
      class: :unknown
    }
  ]
}

Am I missing something or is there indeed a behavior change, and is it expected? Is it documented somewhere I didn’t find?

Thanks in advance!

Can you try configuring in the action atomic_upgrade? false and see if that resolves the issue? This will help isolate the cause.

I have a suspicion of what the error is. Can you try main of ash and see if that resolves the issue?

Actually…I’m not so sure that is the issue :slight_smile: The action you’re showing couldn’t get an atomic upgrade anyway because of the function change. So the issue must be somewhere else.

Alright, try main now, I think I fixed the actual issue :slight_smile:

1 Like

The first two solutions didn’t change anything, but your latest 2316b7b commit fixed the issue.

Thanks again Zach, you rock!

P.S. I’m still having issues believing you’re not a super-AI from the future, how do you do it :joy:

1 Like